Перейти к основному содержимому

Добавление адаптера платформы

В этом руководстве описывается добавление новой платформы обмена сообщениями в шлюз VibeOS. Адаптер платформы подключает VibeOS к внешнему сервису обмена сообщениями (Telegram, Discord, WeCom и т. д.), позволяя пользователям взаимодействовать с агентом через этот сервис.

подсказка

Есть два способа добавить платформу:

  • Плагин (рекомендуется для сообщества/сторонних разработчиков): поместите каталог плагина в ~/.vibeos/plugins/ — никаких изменений в основном коде не требуется. См. раздел Путь плагина ниже.
  • Встроенный: измените 20+ файлов в коде, конфигурации и документации. Используйте Контрольный список встроенного пути ниже.

Обзор архитектуры​

Пользователь ↔ Платформа обмена сообщениями ↔ Адаптер платформы ↔ Исполнитель шлюза ↔ AIAgent

Каждый адаптер расширяет BasePlatformAdapter из gateway/platforms/base.py и реализует:

  • connect() — Установка соединения (WebSocket, long-poll, HTTP-сервер и т. д.) (абстрактный)
  • disconnect() — Корректное завершение работы (абстрактный)
  • send() — Отправка текстового сообщения в чат (абстрактный)
  • send_typing() — Отображение индикатора набора текста (необязательное переопределение)
  • get_chat_info() — Возврат метаданных чата (необязательное переопределение)

Входящие сообщения принимаются адаптером и перенаправляются через self.handle_message(event), который базовый класс направляет исполнителю шлюза.

Путь плагина (рекомендуется)​

Система плагинов позволяет добавить адаптер платформы без изменения основного кода VibeOS. Ваш плагин представляет собой каталог с двумя файлами:

~/.vibeos/plugins/my-platform/
plugin.yaml # Метаданные плагина
adapter.py # Класс адаптера + точка входа register()

plugin.yaml​

Метаданные плагина. Блоки requires_env и optional_env автоматически заполняют записи в UI vibeos config (см. раздел Отображение переменных окружения в vibeos config ниже).

name: my-platform
label: My Platform
kind: platform
version: 1.0.0
description: Мой собственный адаптер платформы обмена сообщениями
author: Ваше Имя
requires_env:
- MY_PLATFORM_TOKEN # простая строка тоже работает
- name: MY_PLATFORM_CHANNEL # или расширенный словарь для лучшего UX
description: "Канал для подключения"
prompt: "Канал"
password: false
optional_env:
- name: MY_PLATFORM_HOME_CHANNEL
description: "Канал по умолчанию для доставки cron"
password: false

adapter.py​

import os
from gateway.platforms.base import (
BasePlatformAdapter, SendResult, MessageEvent, MessageType,
)
from gateway.config import Platform, PlatformConfig


class MyPlatformAdapter(BasePlatformAdapter):
def __init__(self, config: PlatformConfig):
super().__init__(config, Platform("my_platform"))
extra = config.extra or {}
self.token = os.getenv("MY_PLATFORM_TOKEN") or extra.get("token", "")

async def connect(self) -> bool:
# Подключение к API платформы, запуск слушателей
self._mark_connected()
return True

async def disconnect(self) -> None:
self._mark_disconnected()

async def send(self, chat_id, content, reply_to=None, metadata=None):
# Отправка сообщения через API платформы
return SendResult(success=True, message_id="...")

async def get_chat_info(self, chat_id):
return {"name": chat_id, "type": "dm"}


def check_requirements() -> bool:
return bool(os.getenv("MY_PLATFORM_TOKEN"))


def validate_config(config) -> bool:
extra = getattr(config, "extra", {}) or {}
return bool(os.getenv("MY_PLATFORM_TOKEN") or extra.get("token"))


def _env_enablement() -> dict | None:
token = os.getenv("MY_PLATFORM_TOKEN", "").strip()
channel = os.getenv("MY_PLATFORM_CHANNEL", "").strip()
if not (token and channel):
return None
seed = {"token": token, "channel": channel}
home = os.getenv("MY_PLATFORM_HOME_CHANNEL")
if home:
seed["home_channel"] = {"chat_id": home, "name": "Home"}
return seed


def register(ctx):
"""Точка входа плагина — вызывается системой плагинов VibeOS."""
ctx.register_platform(
name="my_platform",
label="My Platform",
adapter_factory=lambda cfg: MyPlatformAdapter(cfg),
check_fn=check_requirements,
validate_config=validate_config,
required_env=["MY_PLATFORM_TOKEN"],
install_hint="pip install my-platform-sdk",
# Автоконфигурация на основе переменных окружения — заполняет
# PlatformConfig.extra из переменных окружения до создания адаптера.
# См. раздел «Автоконфигурация на основе переменных окружения» ниже.
env_enablement_fn=_env_enablement,
# Поддержка доставки cron в домашний канал. Позволяет заданиям cron
# с deliver=my_platform маршрутизироваться без редактирования
# cron/scheduler.py. См. раздел «Доставка cron» ниже.
cron_deliver_env_var="MY_PLATFORM_HOME_CHANNEL",
# Переменные окружения для авторизации пользователей на платформе
allowed_users_env="MY_PLATFORM_ALLOWED_USERS",
allow_all_env="MY_PLATFORM_ALLOW_ALL_USERS",
# Ограничение длины сообщения для интеллектуальной разбивки (0 = без ограничений)
max_message_length=4000,
# Подсказка для LLM, вставляемая в системный промпт
platform_hint=(
"You are chatting via My Platform. "
"It supports markdown formatting."
),
# Отображение
emoji="💬",
)

# Опционально: регистрация инструментов, специфичных для платформы
ctx.register_tool(
name="my_platform_search",
toolset="my_platform",
schema={...},
handler=my_search_handler,
)

Конфигурация​

Пользователи настраивают платформу в config.yaml:

gateway:
platforms:
my_platform:
enabled: true
extra:
token: "..."
channel: "#general"

Или через переменные окружения (которые адаптер читает в __init__).

Что система плагинов обрабатывает автоматически​

Когда вы вызываете ctx.register_platform(), следующие точки интеграции обрабатываются за вас — никаких изменений в основном коде не требуется:

Точка интеграцииКак это работает
Создание адаптера шлюзаРеестр проверяется до встроенной цепочки if/elif
Разбор конфигурацииPlatform._missing_() принимает любое имя платформы
Валидация подключенной платформыВызывается validate_config() из реестра
Авторизация пользователейПроверяются allowed_users_env / allow_all_env
Автовключение через envenv_enablement_fn заполняет PlatformConfig.extra + home_channel
Мост YAML-конфигурацииapply_yaml_config_fn преобразует ключи config.yaml в переменные окружения / extras
Доставка croncron_deliver_env_var делает deliver=<имя>` работоспособным
Записи в UI vibeos configrequires_env / optional_env в plugin.yaml автоматически заполняются
Инструмент send_messageМаршрутизируется через активный адаптер шлюза
Кросс-платформенная доставка вебхуковРеестр проверяется на известные платформы
Доступ к команде /updateФлаг allow_update_command
Каталог каналовПлатформы-плагины включаются в перечисление
Подсказки в системном промптеplatform_hint вставляется в контекст LLM
Разбивка сообщенийmax_message_length для интеллектуального разделения
Редактирование PIIФлаг pii_safe
vibeos statusПоказывает платформы-плагины с тегом (plugin)
vibeos gateway setupПлатформы-плагины появляются в меню настройки
vibeos tools / vibeos skillsПлатформы-плагины в конфигурации для каждой платформы
Блокировка токена (мультипрофиль)Используйте acquire_scoped_lock() в вашем connect()
Предупреждение об осиротевшей конфигурацииИнформационное сообщение в логе, когда плагин отсутствует

Автоконфигурация на основе переменных окружения​

Большинство пользователей настраивают платформу, помещая переменные окружения в ~/.vibeos/.env, а не редактируя config.yaml. Хук env_enablement_fn позволяет вашему плагину подхватывать эти переменные окружения до создания адаптера, так что vibeos gateway status, get_connected_platforms() и доставка cron видят корректное состояние без инстанцирования SDK платформы.

def _env_enablement() -> dict | None:
"""Заполняет PlatformConfig.extra из переменных окружения.

Вызывается реестром платформ во время load_gateway_config().
Возвращает None, если платформа минимально не настроена —
вызывающий код пропускает автовключение. Возвращает словарь
для заполнения extras.

Специальный ключ 'home_channel' извлекается и становится
датаклассом HomeChannel в PlatformConfig; все остальные ключи
объединяются в PlatformConfig.extra.
"""
token = os.getenv("MY_PLATFORM_TOKEN", "").strip()
channel = os.getenv("MY_PLATFORM_CHANNEL", "").strip()
if not (token and channel):
return None
seed = {"token": token, "channel": channel}
home = os.getenv("MY_PLATFORM_HOME_CHANNEL")
if home:
seed["home_channel"] = {
"chat_id": home,
"name": os.getenv("MY_PLATFORM_HOME_CHANNEL_NAME", "Home"),
}
return seed


def register(ctx):
ctx.register_platform(
name="my_platform",
label="My Platform",
adapter_factory=lambda cfg: MyPlatformAdapter(cfg),
check_fn=check_requirements,
validate_config=validate_config,
env_enablement_fn=_env_enablement,
# ... остальные поля
)

Мост YAML → env​

Некоторые пользователи предпочитают задавать ключи в config.yaml (my_platform.require_mention, my_platform.allowed_channels и т. д.) вместо переменных окружения. Хук apply_yaml_config_fn позволяет вашему плагину взять на себя это преобразование, вместо того чтобы заставлять основной gateway/config.py знать схему YAML вашей платформы.

import os

def _apply_yaml_config(yaml_cfg: dict, platform_cfg: dict) -> dict | None:
"""Преобразует ключи `my_platform:` из config.yaml в переменные окружения / extras.

yaml_cfg — полный разобранный словарь config.yaml верхнего уровня
platform_cfg — под-словарь платформы (yaml_cfg.get("my_platform", {}))

Может напрямую изменять os.environ (используйте проверки
`not os.getenv(...)` для сохранения приоритета env > YAML)
и/или возвращать словарь для объединения с PlatformConfig.extra.
Возвращает None или {} если extras не нужны.
"""
if "require_mention" in platform_cfg and not os.getenv("MY_PLATFORM_REQUIRE_MENTION"):
os.environ["MY_PLATFORM_REQUIRE_MENTION"] = str(platform_cfg["require_mention"]).lower()
allowed = platform_cfg.get("allowed_channels")
if allowed is not None and not os.getenv("MY_PLATFORM_ALLOWED_CHANNELS"):
if isinstance(allowed, list):
allowed = ",".join(str(v) for v in allowed)
os.environ["MY_PLATFORM_ALLOWED_CHANNELS"] = str(allowed)
return None # ничего дополнительного для объединения с PlatformConfig.extra

def register(ctx):
ctx.register_platform(
name="my_platform",
...,
apply_yaml_config_fn=_apply_yaml_config,
)

Хук вызывается во время load_gateway_config() после общего цикла обработки общих ключей (который обрабатывает такие общие ключи, как unauthorized_dm_behavior, notice_delivery, reply_prefix, require_mention и т. д.) и до _apply_env_overrides(), так что вашему плагину нужно обрабатывать только специфичные для платформы ключи.

Исключения, вызванные хуком, перехватываются и логируются на уровне отладки — некорректно работающий плагин никогда не прерывает загрузку конфигурации шлюза.

Доставка cron​

Чтобы задания cron с deliver=my_platform маршрутизировались в настроенный домашний канал, установите cron_deliver_env_var в имя переменной окружения, содержащей ID чата/комнаты/канала по умолчанию:

ctx.register_platform(
name="my_platform",
...
cron_deliver_env_var="MY_PLATFORM_HOME_CHANNEL",
)

Планировщик читает эту переменную окружения при определении домашнего целевого канала для заданий deliver=my_platform, а также рассматривает платформу как допустимую цель cron в проверках стиля _KNOWN_DELIVERY_PLATFORMS. Если ваш env_enablement_fn заполняет словарь home_channel (см. выше), он имеет приоритет — cron_deliver_env_var является запасным вариантом для заданий cron, которые выполняются до заполнения переменных окружения.

Внепроцессная доставка cron​

cron_deliver_env_var делает вашу платформу распознаваемой целью deliver=. Чтобы фактическая отправка прошла успешно, когда задание cron выполняется в отдельном процессе от шлюза (т. е. vibeos cron run отдельно от vibeos gateway), зарегистрируйте standalone_sender_fn:

async def _standalone_send(
pconfig,
chat_id,
message,
*,
thread_id=None,
media_files=None,
force_document=False,
):
"""Открыть эфемерное соединение / получить свежий токен, отправить и закрыть."""
# ... открыть соединение, отправить сообщение, вернуть результат ...
return {"success": True, "message_id": "..."}
# или {"error": "..."}

ctx.register_platform(
name="my_platform",
...
cron_deliver_env_var="MY_PLATFORM_HOME_CHANNEL",
standalone_sender_fn=_standalone_send,
)

Зачем нужен этот хук: встроенные платформы (Telegram, Discord, Slack и т. д.) содержат прямые REST-помощники в tools/send_message_tool.py, поэтому cron может доставлять сообщения, не удерживая шлюз в том же процессе. Платформы-плагины исторически зависели от _gateway_runner_ref(), который возвращает None вне процесса шлюза, поэтому без standalone_sender_fn отправка со стороны cron завершается ошибкой «Нет активного адаптера для платформы '&lt;имя&gt;'».

Функция получает те же pconfig и chat_id, что и активный адаптер, а также необязательные аргументы ключевых слов thread_id, media_files и force_document. Возврат {"success": True, "message_id": ...} рассматривается как успешная доставка; возврат {"error": "..."} отображает сообщение в delivery_errors cron. Исключения, вызванные внутри функции, перехватываются диспетчером и сообщаются как «Сбой отправки автономного плагина: <причина>». Эталонные реализации находятся в plugins/platforms/{irc,teams,google_chat}/adapter.py.

Отображение переменных окружения в vibeos config​

vibeos_cli/config.py сканирует plugins/platforms/*/plugin.yaml во время импорта и автоматически заполняет OPTIONAL_ENV_VARS из блоков requires_env и (необязательно) optional_env. Используйте расширенную форму словаря, чтобы предоставить правильные описания, подсказки, флаги паролей и URL — UI настройки CLI подхватит их автоматически.

# plugins/platforms/my_platform/plugin.yaml
name: my_platform-platform
label: My Platform
kind: platform
version: 1.0.0
description: >
Адаптер шлюза My Platform для VibeOS.
author: Ваше Имя
requires_env:
- name: MY_PLATFORM_TOKEN
description: "Токен бота из консоли My Platform"
prompt: "Токен бота My Platform"
url: "https://my-platform.example.com/bots"
password: true
- name: MY_PLATFORM_CHANNEL
description: "Канал для подключения (например, #vibeos)"
prompt: "Канал"
password: false
optional_env:
- name: MY_PLATFORM_HOME_CHANNEL
description: "Канал по умолчанию для доставки cron (по умолчанию MY_PLATFORM_CHANNEL)"
prompt: "Домашний канал (или оставить пустым)"
password: false
- name: MY_PLATFORM_ALLOWED_USERS
description: "ID пользователей, разделенные запятыми, которым разрешено общаться с ботом"
prompt: "Разрешенные пользователи (через запятую)"
password: false

Поддерживаемые ключи словаря: name (обязательный), description, prompt, url, password (bool; автоматически определяется по суффиксу *_TOKEN / *_SECRET / *_KEY / *_PASSWORD / *_JSON, если опущен), category (по умолчанию "messaging").

Записи в виде простых строк (- MY_PLATFORM_TOKEN) по-прежнему работают — для них генерируется общее описание, автоматически полученное из label плагина. Если жестко заданная запись для той же переменной уже существует в OPTIONAL_ENV_VARS, она имеет приоритет (обратная совместимость); форма plugin.yaml действует как запасной вариант.

Специфичный для платформы UX медленного LLM​

Некоторые платформы имеют ограничения, которые меняют способ представления медленного ответа LLM:

  • LINE выдает одноразовый токен ответа, срок действия которого истекает примерно через 60 секунд после входящего события. Ответ с использованием этого токена бесплатен; возврат к тарифицируемому Push API — нет. Если LLM не завершил работу к крайнему сроку, выбор стоит между «сжечь платную квоту Push» или «сделать что-то более умное с токеном ответа до его истечения».
  • WhatsApp помечает сеанс как неактивный через 24 часа, после чего принимаются только шаблонные сообщения.
  • SMS не имеет понятия индикаторов набора текста или прогрессивных обновлений — длинные ответы просто выглядят так, будто бот офлайн.

Это реальные ограничения, которые базовый BasePlatformAdapter не может предвидеть. Поверхность плагина намеренно оставляет место для того, чтобы адаптер мог наложить специфичный для платформы UX поверх базового цикла набора текста, не расширяя список аргументов.

Паттерн: подкласс _keep_typing для наслоения промежуточного UX​

BasePlatformAdapter._keep_typing — это пульс индикатора набора текста; он работает как фоновая задача, пока LLM генерирует ответ, и отменяется, когда ответ доставлен. Чтобы добавить поведение, специфичное для платформы, при достижении порога (например, отправить пузырек «все еще думаю» через 45 секунд), переопределите _keep_typing в вашем адаптере, запланируйте свою собственную задачу вместе с super()._keep_typing() и завершите ее в finally:

class LineAdapter(BasePlatformAdapter):
async def _keep_typing(self, chat_id: str, *args, **kwargs) -> None:
if self.slow_response_threshold <= 0:
await super()._keep_typing(chat_id, *args, **kwargs)
return

async def _fire_at_threshold() -> None:
try:
await asyncio.sleep(self.slow_response_threshold)
except asyncio.CancelledError:
raise
# Специфичная для платформы работа здесь — для LINE отправьте
# пузырек с шаблонными кнопками «Получить ответ», используя
# кэшированный токен ответа, чтобы пользователь мог получить
# кэшированный ответ позже через свежий (бесплатный) токен
# ответа из обратного вызова postback.
await self._send_slow_response_button(chat_id)

side_task = asyncio.create_task(_fire_at_threshold())
try:
await super()._keep_typing(chat_id, *args, **kwargs)
finally:
if not side_task.done():
side_task.cancel()
try:
await side_task
except (asyncio.CancelledError, Exception):
pass

Ключевые моменты:

  • Всегда используйте await super()._keep_typing(...). Пульс набора текста независимо полезен — не заменяйте его, наслаивайтесь поверх него.
  • Завершайте побочную задачу в finally. Когда LLM завершает работу (или /stop отменяет выполнение), шлюз отменяет задачу набора текста. Ваша побочная задача также должна реагировать на эту отмену, иначе она останется висеть и может сработать после того, как ответ уже был доставлен.
  • Сопрягите с interrupt_session_activity для разрешения любого осиротевшего состояния UX, когда пользователь отправляет /stop. Для LINE это означает перевод записи в кэше postback из PENDING в ERROR, чтобы постоянная кнопка «Получить ответ» доставляла сообщение «Выполнение было прервано» вместо зацикливания.

Паттерн: подкласс send для маршрутизации через кэш вместо немедленной отправки​

Если ваш UX медленного ответа кэширует ответ для последующего извлечения (поток postback в LINE), ваше переопределение send должно распознавать три режима:

  1. Ожидающий postback активен для этого чата → кэшируйте ответ под request_id, не отправляйте ничего видимого.
  2. Системное подтверждение занятости (⚡ Interrupting, ⏳ Queued, ⏩ Steered) → обходите кэш и отправляйте видимо, чтобы пользователь видел ответ шлюза на свой ввод.
  3. Нормальный ответ → отправляйте через токен ответа или Push как обычно.
async def send(self, chat_id: str, content: str, **kw) -> SendResult:
if _is_system_bypass(content):
return await self._send_text_chunks(chat_id, content, force_push=False)
pending_rid = self._pending_buttons.get(chat_id)
if pending_rid:
self._cache.set_ready(pending_rid, content)
return SendResult(success=True, message_id=pending_rid)
return await self._send_text_chunks(chat_id, content, force_push=False)

_SYSTEM_BYPASS_PREFIXES — это собственные префиксы подтверждения занятости шлюза (⚡, ⏳, ⏩, 💾). Всегда пропускайте их видимо, независимо от состояния кэшированного UX.

Когда этот паттерн уместен​

Используйте подход с переопределением цикла набора текста, когда:

  • Исходящий API платформы имеет жесткое временное ограничение (одноразовый токен ответа, истекающая липкая сессия и т. д.) И
  • Видимый промежуточный пузырек приемлем с точки зрения UX на этой платформе.

Используйте более простой путь slow_response_threshold = 0 (всегда Push), когда:

  • Платформа не имеет значимого различия между бесплатным и платным, ИЛИ
  • Сообщество пользователей предпочитает тишину «загрузка… загрузка… ГОТОВО» с последующим ответом, а не интерактивный промежуточный пузырек.

LINE поддерживает оба варианта: порог по умолчанию составляет 45 секунд для бесплатного получения postback, а LINE_SLOW_RESPONSE_THRESHOLD=0 возвращается к «всегда Push как запасной вариант».

Эталонная реализация​

См. plugins/platforms/line/adapter.py для полной реализации postback LINE — конечный автомат RequestCache (PENDING → READY → DELIVERED, плюс ERROR для /stop), переопределение _keep_typing, которое запускает пузырек с шаблонными кнопками при достижении порога, переопределение send, которое маршрутизирует через кэш, и переопределение interrupt_session_activity, которое разрешает осиротевшие записи PENDING.

Эталонные реализации (путь плагина)​

См. plugins/platforms/irc/ в репозитории для полного рабочего примера — полноценный асинхронный IRC-адаптер без внешних зависимостей. plugins/platforms/teams/ охватывает Bot Framework / Adaptive Cards, plugins/platforms/google_chat/ охватывает REST API на основе OAuth, а plugins/platforms/line/ охватывает вебхук-ориентированные Messaging API со специфичным для платформы UX медленного LLM.


Пошаговый контрольный список (встроенный путь)​

примечание

Этот контрольный список предназначен для добавления платформы непосредственно в основную кодовую базу VibeOS — обычно выполняется основными участниками для официально поддерживаемых платформ. Сторонние платформы сообщества должны использовать Путь плагина выше.

1. Перечисление платформ​

Добавьте свою платформу в перечисление Platform в gateway/config.py:

class Platform(str, Enum):
# ... существующие платформы ...
NEWPLAT = "newplat"

2. Файл адаптера​

Создайте plugins/platforms/newplat/adapter.py:

from gateway.config import Platform, PlatformConfig
from gateway.platforms.base import (
BasePlatformAdapter, MessageEvent, MessageType, SendResult,
)

def check_newplat_requirements() -> bool:
"""Возвращает True, если зависимости доступны."""
return SOME_SDK_AVAILABLE

class NewPlatAdapter(BasePlatformAdapter):
def __init__(self, config: PlatformConfig):
super().__init__(config, Platform.NEWPLAT)
# Чтение конфигурации из словаря config.extra
extra = config.extra or {}
self._api_key = extra.get("api_key") or os.getenv("NEWPLAT_API_KEY", "")

async def connect(self) -> bool:
# Настройка соединения, запуск опроса/вебхука
self._mark_connected()
return True

async def disconnect(self) -> None:
self._running = False
self._mark_disconnected()

async def send(self, chat_id, content, reply_to=None, metadata=None):
# Отправка сообщения через API платформы
return SendResult(success=True, message_id="...")

async def get_chat_info(self, chat_id):
return {"name": chat_id, "type": "dm"}

Для входящих сообщений создайте MessageEvent и вызовите self.handle_message(event):

source = self.build_source(
chat_id=chat_id,
chat_name=name,
chat_type="dm", # или "group"
user_id=user_id,
user_name=user_name,
)
event = MessageEvent(
text=content,
message_type=MessageType.TEXT,
source=source,
message_id=msg_id,
)
await self.handle_message(event)

3. Конфигурация шлюза (gateway/config.py)​

Три точки касания:

  1. get_connected_platforms() — Добавьте проверку обязательных учетных данных вашей платформы
  2. load_gateway_config() — Добавьте запись в карту токенов env: Platform.NEWPLAT: "NEWPLAT_TOKEN"
  3. _apply_env_overrides() — Сопоставьте все переменные окружения NEWPLAT_* с конфигурацией

4. Исполнитель шлюза (gateway/run.py)​

Пять точек касания:

  1. _create_adapter() — Добавьте ветвь elif platform == Platform.NEWPLAT:
  2. Карта _is_user_authorized() allowed_users — Platform.NEWPLAT: "NEWPLAT_ALLOWED_USERS"
  3. Карта _is_user_authorized() allow_all — Platform.NEWPLAT: "NEWPLAT_ALLOW_ALL_USERS"
  4. Ранняя проверка env кортеж _any_allowlist — Добавьте "NEWPLAT_ALLOWED_USERS"
  5. Ранняя проверка env кортеж _allow_all — Добавьте "NEWPLAT_ALLOW_ALL_USERS"
  6. frozenset _UPDATE_ALLOWED_PLATFORMS — Добавьте Platform.NEWPLAT

5. Кросс-платформенная доставка​

  1. gateway/platforms/webhook.py — Добавьте "newplat" в кортеж типа доставки
  2. cron/scheduler.py — Добавьте в frozenset _KNOWN_DELIVERY_PLATFORMS и карту платформ _deliver_result()

6. Интеграция с CLI​

  1. vibeos_cli/config.py — Добавьте все переменные NEWPLAT_* в _EXTRA_ENV_KEYS
  2. vibeos_cli/gateway.py — Добавьте запись в список _PLATFORMS с ключом, меткой, эмодзи, token_var, инструкциями по настройке и vars
  3. vibeos_cli/platforms.py — Добавьте запись PlatformInfo с меткой и default_toolset (используется в TUI skills_config и tools_config)
  4. vibeos_cli/setup.py — Добавьте функцию _setup_newplat() (может делегировать gateway.py) и добавьте кортеж в список платформ обмена сообщениями
  5. vibeos_cli/status.py — Добавьте запись обнаружения платформы: "NewPlat": ("NEWPLAT_TOKEN", "NEWPLAT_HOME_CHANNEL")
  6. vibeos_cli/dump.py — Добавьте "newplat": "NEWPLAT_TOKEN" в словарь обнаружения платформ

7. Инструменты​

  1. tools/send_message_tool.py — Добавьте "newplat": Platform.NEWPLAT в карту платформ
  2. tools/cronjob_tools.py — Добавьте newplat в строку описания целевой доставки

8. Наборы инструментов​

  1. toolsets.py — Добавьте определение набора инструментов "vibeos-newplat" с `_HER