Добавление адаптера платформы
В этом руководстве описывается добавление новой платформы обмена сообщениями в шлюз 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 |
| Автовключение через env | env_enablement_fn заполняет PlatformConfig.extra + home_channel |
| Мост YAML-конфигурации | apply_yaml_config_fn преобразует ключи config.yaml в переменные окружения / extras |
| Доставка cron | cron_deliver_env_var делает deliver=<имя>` работоспособным |
Записи в UI vibeos config | requires_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 завершается ошибкой «Нет активного адаптера для платформы '<имя>'».
Функция получает те же 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 должно распознавать три режима:
- Ожидающий postback активен для этого чата → кэшируйте ответ под request_id, не отправляйте ничего видимого.
- Системное подтверждение занятости (
⚡ Interrupting,⏳ Queued,⏩ Steered) → обходите кэш и отправляйте видимо, чтобы пользователь видел ответ шлюза на свой ввод. - Нормальный ответ → отправляйте через токен ответа или 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)
Три точки касания:
get_connected_platforms()— Добавьте проверку обязательных учетных данных вашей платформыload_gateway_config()— Добавьте запись в карту токенов env:Platform.NEWPLAT: "NEWPLAT_TOKEN"_apply_env_overrides()— Сопоставьте все переменные окруженияNEWPLAT_*с конфигурацией
4. Исполнитель шлюза (gateway/run.py)
Пять точек касания:
_create_adapter()— Добавьте ветвьelif platform == Platform.NEWPLAT:- Карта
_is_user_authorized()allowed_users —Platform.NEWPLAT: "NEWPLAT_ALLOWED_USERS" - Карта
_is_user_authorized()allow_all —Platform.NEWPLAT: "NEWPLAT_ALLOW_ALL_USERS" - Ранняя проверка env кортеж
_any_allowlist— Добавьте"NEWPLAT_ALLOWED_USERS" - Ранняя проверка env кортеж
_allow_all— Добавьте"NEWPLAT_ALLOW_ALL_USERS" - frozenset
_UPDATE_ALLOWED_PLATFORMS— ДобавьтеPlatform.NEWPLAT
5. Кросс-платформенная доставка
gateway/platforms/webhook.py— Добавьте"newplat"в кортеж типа доставкиcron/scheduler.py— Добавьте в frozenset_KNOWN_DELIVERY_PLATFORMSи карту платформ_deliver_result()
6. Интеграция с CLI
vibeos_cli/config.py— Добавьте все переменныеNEWPLAT_*в_EXTRA_ENV_KEYSvibeos_cli/gateway.py— Добавьте запись в список_PLATFORMSс ключом, меткой, эмодзи, token_var, инструкциями по настройке и varsvibeos_cli/platforms.py— Добавьте записьPlatformInfoс меткой и default_toolset (используется в TUIskills_configиtools_config)vibeos_cli/setup.py— Добавьте функцию_setup_newplat()(может делегироватьgateway.py) и добавьте кортеж в список платформ обмена сообщениямиvibeos_cli/status.py— Добавьте запись обнаружения платформы:"NewPlat": ("NEWPLAT_TOKEN", "NEWPLAT_HOME_CHANNEL")vibeos_cli/dump.py— Добавьте"newplat": "NEWPLAT_TOKEN"в словарь обнаружения платформ
7. Инструменты
tools/send_message_tool.py— Добавьте"newplat": Platform.NEWPLATв карту платформtools/cronjob_tools.py— Добавьтеnewplatв строку описания целевой доставки
8. Наборы инструментов
toolsets.py— Добавьте определение набора инструментов"vibeos-newplat"с `_HER