Перейти к содержимому
OmniRoute source

Embedded Services (Русский)

  1. Обзор
  2. Архитектура — 4 уровня
  3. Конечный автомат жизненного цикла
  4. Справочник API
  5. Безопасность
  6. Добавление нового встроенного сервиса
  7. Устранение неполадок
  8. Часто задаваемые вопросы

Встроено шесть сервисов:

Сервис npm-пакет Порт по умолчанию Назначение
9Router 9router 20130 ИИ-маршрутизатор, который OmniRoute может использовать как дополнительного провайдера. Модели представлены в виде 9router/{sub}/{model}
CLIProxyAPI Бинарный файл из релиза GitHub (cliproxy) 8317 Локальный прокси-адаптер для потоков аутентификации Anthropic CLI. Обеспечивает резервную маршрутизацию при истечении срока действия OAuth-токенов
Mux mux (безголовый mux server) 8322 Локальный демон оркестрации агентов (coder/mux). Управляется только его жизненный цикл — он не является целью маршрутизации (без проксирования LLM).
Bifrost @maximhq/bifrost 8080 Ретрансляционный бэкенд ИИ-шлюза на Go. Во время работы автоматически выбирается маршрутом ретрансляции (/v1/relay/)
Dario @askalf/dario 3456 Прокси подписки Claude — альтернатива/резервный вариант CLIProxyAPI для трафика в формате Claude Code; внедрённый ключ становится DARIO_ADMIN_TOKEN, ограничивающим доступ к OAuth-интерфейсу управления /admin/*
open-wa @open-wa/wa-automate 8323 Автоматизация WhatsApp Web (безголовый Chromium через Puppeteer). Управляется только его жизненный цикл — он не является целью маршрутизации.

Все шесть сервисов используют одну и ту же модель супервизорного управления:

  • OmniRoute устанавливает их в DATA_DIR/services/{name}/ (изолированно от собственного package.json OmniRoute)
  • OmniRoute запускает и отслеживает их как дочерние процессы
  • OmniRoute передаёт эфемерный API-ключ в окружение дочернего процесса и ротирует его без простоя (где применимо)
  • Все маршруты управления (/api/services/*) доступны только локально (LOCAL_ONLY) — исключительно через loopback-интерфейс (жёсткое правило №17)

Ключевые решения (из плана проектирования)

Заголовок раздела «Ключевые решения (из плана проектирования)»
Решение Значение
Доступ панели управления к нативному интерфейсу 9Router Обратный прокси по адресу /dashboard/providers/services/9router/embed/*
Механизм установки npm install {package} через execFile (без интерполяции оболочки)
Режим использования Провайдер зарегистрирован в механизме маршрутизации как 9router/{sub}/{model}
Управление API-ключами OmniRoute генерирует, шифрует при хранении (AES-256-GCM) и передаёт через переменные окружения
Расположение в панели управления /dashboard/providers/services (три вкладки)
Автозапуск Переключатель для каждого сервиса, по умолчанию выключен

┌────────────────────────────────────────────────────────────────────┐
│ Слой 1 — UI │
│ /dashboard/providers/services (вкладки: CLIProxyAPI | 9Router | Mux)│
│ Логи в реальном времени (SSE), запуск/остановка/перезапуск/обновление, настройки, установка│
│ │
│ src/app/(dashboard)/dashboard/providers/services/ │
│ ├── page.tsx Оболочка + маршрутизация вкладок через ?tab=│
│ ├── tabs/ CliproxyServiceTab, NinerouterServiceTab,│
│ │ MuxServiceTab │
│ └── components/ ServiceStatusCard, ServiceLifecycleButtons,│
│ ServiceLogsPanel, ApiKeyCard, ... │
└──────────────────────┬─────────────────────────────────────────────┘
│ HTTP (Next.js fetch)
┌──────────────────────▼─────────────────────────────────────────────┐
│ Слой 2 — API (LOCAL_ONLY — только loopback) │
│ │
│ /api/services/9router/{install|start|stop|restart|update| │
│ rotate-key|status|auto-start|logs} │
│ /api/services/cliproxy/{install|start|stop|restart|update| │
│ status|auto-start|logs} │
│ /api/services/mux/{install|start|stop|restart|update| │
│ status|auto-start|logs} │
│ /dashboard/providers/services/9router/embed/[...path] │
│ (обратный прокси HTTP + WebSocket → upstream 9Router) │
│ │
│ Проверка: LOCAL_ONLY_API_PREFIXES включает "/api/services/" и │
│ "/dashboard/providers/services/*/embed/" │
└──────────────────────┬─────────────────────────────────────────────┘
│ внутрипроцессные вызовы
┌──────────────────────▼─────────────────────────────────────────────┐
│ Слой 3 — ServiceSupervisor (src/lib/services/) │
│ │
│ ServiceSupervisor.ts Универсальный супервизор (child_process.spawn)│
│ ├── установка: execFile('npm', ['install', pkg, '--prefix']) │
│ ├── запуск: spawn(node, [entrypoint], {env, cwd}) │
│ ├── ключ API: crypto.randomBytes(32) → env NINEROUTER_API_KEY │
│ ├── порт: 20130 для 9Router (настраиваемый) │
│ ├── логи: кольцевой буфер stdio 5 МБ → события SSE │
│ ├── состояние: HTTP GET /health каждые 2–5 с, ленивое восстановление│
│ └── жизненный цикл: SIGTERM 15 с → SIGKILL │
│ │
│ registry.ts getSupervisor(name) / registerSupervisor() │
│ bootstrap.ts Инициализирует все SERVICES[] при запуске процесса│
│ apiKey.ts getOrCreateApiKey(), generateServiceApiKey() │
│ modelSync.ts Периодический GET /v1/models → таблица service_models│
│ ringBuffer.ts Кольцевой буфер логов (5 МБ на сервис) │
│ healthCheck.ts Периодическая HTTP-проверка состояния │
│ installers/ ninerouter.ts, cliproxy.ts, mux.ts, openwa.ts │
│ (адаптеры установщиков) │
└──────────────────────┬─────────────────────────────────────────────┘
│ HTTP, совместимый с OpenAI (loopback)
┌──────────────────────▼─────────────────────────────────────────────┐
│ Слой 4 — Провайдер / маршрутизация │
│ │
│ open-sse/executors/ninerouter.ts │
│ Повторно получает порт и ключ API для каждого запроса (без кэширования).│
│ Удаляет префикс "9router/" из идентификатора модели перед проксированием.│
│ Возвращает 503 service_not_running, если супервизор не в состоянии "running".│
│ │
│ src/shared/constants/providers.ts │
│ Запись для "9router": isEmbeddedService: true │
│ │
│ open-sse/config/providerRegistry.ts │
│ Модели хранятся как "9router/{sub}/{model}" (с префиксом). │
│ Синхронизируются каждые 5 мин. через modelSync.ts. │
│ │
│ Mux управляется ТОЛЬКО на уровне жизненного цикла (слои 1–3) — это │
│ демон оркестрации агентов, а не LLM-прокси, поэтому у него нет │
│ исполнителя/провайдера слоя 4, и он никогда не является целью маршрутизации.│
└────────────────────────────────────────────────────────────────────┘
Файл Роль
src/lib/services/ServiceSupervisor.ts Основной класс: жизненный цикл, блокировка, проверка состояния, кольцевой буфер
src/lib/services/bootstrap.ts Регистрация на уровне процесса и автоматический запуск
src/lib/services/registry.ts Карта-синглтон tool → supervisor
src/lib/services/apiKey.ts Генерация ключей, шифрование AES-256-GCM при хранении
src/lib/services/modelSync.ts Периодическая синхронизация моделей (5 мин) + по запросу
src/lib/services/ringBuffer.ts Кольцевой буфер журналов размером 5 МБ с подпиской через SSE
src/lib/services/healthCheck.ts Проверка доступности по HTTP (настраиваемый интервал)
src/lib/services/installers/ninerouter.ts Установка/обновление/удаление 9Router через npm
src/lib/services/installers/cliproxy.ts Установка/обновление/удаление CLIProxyAPI через npm
src/lib/services/installers/mux.ts Установка/обновление/удаление Mux через npm
src/lib/services/installers/openwa.ts Установка/обновление/удаление open-wa через npm
src/app/api/services/9router/_lib.ts Вспомогательная функция getOrInitSupervisor()
src/app/api/services/[name]/logs/route.ts Общая конечная точка SSE для журналов
open-sse/executors/ninerouter.ts Исполнитель провайдера (уровень 4)

install()
┌─────────────┐ ──────────► ┌─────────────┐
│ not_installed│ │ stopped │◄──────────────────┐
└─────────────┘ └──────┬──────┘ │
│ start() │
▼ │ stop()
┌──────────┐ │
│ starting │ │
└────┬─────┘ │
проверка здоровья успешна │ сбой / SIGTERM │
┌────▼─────┐ (выход в течение 5 с)│
│ running │──── сбой ───────────►┤
└────┬─────┘ ┌─▼────┐
stop() │ │error │
▼ └──────┘
┌──────────┐
│ stopping │
└──────────┘

Состояния хранятся в таблице БД version_manager (столбец status) и дублируются в состоянии ServiceSupervisor в памяти. Для запущенного процесса состояние в памяти является авторитетным; состояние в БД служит постоянным резервным источником при загрузке.

Из Событие В
not_installed install() выполнен успешно stopped
stopped вызван start() starting
starting проверка здоровья возвращает 200 running
starting процесс завершается до готовности error
running вызван stop() stopping → stopped
running процесс неожиданно завершается (< 5 с) error (быстрый сбой)
running процесс неожиданно завершается (> 5 с) error
error вызван start() starting
любое stop() во время stopping без действия

ServiceSupervisor сериализует операции жизненного цикла с помощью асинхронной блокировки операций (withLock()). Одновременные вызовы start() для одного супервизора приводят к запуску ровно одного процесса; второй вызывающий ожидает и получает существующий статус. Это предотвращает состояния гонки, например, когда автозапуск и кнопка в пользовательском интерфейсе срабатывают одновременно.


Все маршруты в /api/services/ доступны LOCAL_ONLY (только через loopback, жёсткое правило #17). Запросы не через loopback получают 403 LOCAL_ONLY независимо от токена аутентификации.

Устанавливает 9Router из npm. Создаёт DATA_DIR/services/9router/ с собственными package.json и node_modules/. Не конфликтует с собственными зависимостями OmniRoute.

Тело запроса (все поля необязательны):

{ "version": "latest" }
Поле Тип По умолчанию Описание
version string "latest" Тег версии npm или semver для установки

Ответы:

Статус Описание
200 { ok: true, installedVersion: "x.y.z", path: "..." }
400 Недопустимое тело запроса (ошибка валидации Zod)
409 Установка уже выполняется (блокировка удерживается)
500 Сбой установки npm — понятное описание см. в message

Примечания: Используется execFile('npm', [...]) — без оболочки и интерполяции (жёсткое правило #13). Ошибки EACCES возвращаются в виде понятных сообщений.


Запускает 9Router. Регистрирует супервизор, если он ещё не зарегистрирован, а затем вызывает supervisor.start(). Идемпотентно, если сервис уже запущен.

Тело запроса: отсутствует

Ответы:

Статус Описание
200 Объект ServiceStatus (см. схему ниже)
409 9Router не установлен (status: "not_installed")
503 Сбой запуска (ошибка процесса — см. lastError)

Схема ServiceStatus:

{
"tool": "9router",
"state": "running",
"pid": 12345,
"port": 20130,
"health": "healthy",
"startedAt": "2026-05-25T10:00:00.000Z",
"lastError": null
}

Корректно останавливает 9Router. Отправляет SIGTERM, ожидает 15 с, а затем отправляет SIGKILL, если процесс всё ещё работает. Идемпотентно, если сервис уже остановлен.

Тело запроса: отсутствует

Ответы:

Статус Описание
200 ServiceStatus (state: “stopped”)
503 Непредвиденный сбой остановки

Эквивалентно последовательному вызову stop() и start() под блокировкой операции.

Тело запроса: отсутствует

Ответы: такие же, как у start (возвращает итоговый ServiceStatus).


Обновляет 9Router до более новой версии npm. Если сервис запущен, сначала он останавливается, затем выполняется npm install (новая версия устанавливается на месте), после чего сервис перезапускается.

Тело запроса (все поля необязательны):

{ "version": "latest" }

Ответы:

Статус Описание
200 { ok: true, previousVersion: "...", installedVersion: "..." }
400 Недопустимое тело запроса
500 Сбой обновления npm

Генерирует новый API-ключ для 9Router, шифрует его при хранении и перезапускает сервис (если он запущен), чтобы тот получил новый ключ из своего окружения. Старый ключ немедленно становится недействительным.

Тело запроса: отсутствует

Ответы:

Статус Описание
200 { keyRotated: true, restarted: boolean }
500 Сбой ротации

Безопасность: Новый ключ никогда не возвращается в ответе (утечки учётных данных нет). Он хранится в зашифрованном виде (AES-256-GCM) в таблице version_manager.


Возвращает объединённый актуальный статус и статус из БД, включая метаданные версии и предварительное представление API-ключа.

Ответы:

Статус Описание
200 См. схему ниже
500 Сбой чтения статуса

Схема ответа:

{
"tool": "9router",
"state": "running",
"pid": 12345,
"port": 20130,
"health": "healthy",
"startedAt": "2026-05-25T10:00:00.000Z",
"lastError": null,
"installedVersion": "1.2.3",
"latestVersion": "1.2.4",
"updateAvailable": true,
"apiKeyMasked": "nr_****abcd",
"autoStart": false,
"providerExpose": false
}

Переключает флаг автоматического запуска. При enabled: true сервис автоматически запустится при следующем запуске OmniRoute (если сервис установлен).

Тело запроса:

{ "enabled": true }

Ответы:

Статус Описание
200 { autoStart: true }
400 Недопустимое тело

SSE-поток актуальных журналов из кольцевого буфера stdout/stderr процесса 9Router.

Параметры запроса:

Параметр Тип По умолчанию Описание
tail integer 200 Количество исторических строк для первоначальной отправки (максимум 1000)
filter string отсутствует Регистронезависимый фильтр по подстроке (без регулярных выражений — безопасно от ReDoS)

События SSE:

Событие Данные Описание
snapshot LogLine[] Начальная выборка последних строк
log LogLine Актуальная строка журнала
heartbeat {} Поддержание соединения каждые 15 с

Схема LogLine:

{
"ts": 1716633600000,
"stream": "stdout",
"line": "[9router] Listening on :20130"
}

Ответы:

Статус Описание
200 text/event-stream
400 Параметр filter слишком длинный (> 200 символов)
404 Сервис не найден (супервизор не зарегистрирован)

CLIProxyAPI имеет ту же структуру эндпоинтов, что и 9Router, за исключением rotate-key, с добавлением accounts, provider-expose и auto-restart-adopted. Теперь при запуске он получает выделенный API-ключ уровня данных (needsApiKey: true в bootstrap.ts, используется для синхронизации моделей); status содержит меньше полей.

Метод Путь Описание
POST /api/services/cliproxy/install Установить CLIProxyAPI из npm
POST /api/services/cliproxy/start Запустить CLIProxyAPI
POST /api/services/cliproxy/stop Остановить CLIProxyAPI
POST /api/services/cliproxy/restart Перезапустить CLIProxyAPI
POST /api/services/cliproxy/update Обновить до более новой версии
GET /api/services/cliproxy/status Текущее состояние + состояние в БД (без apiKeyMasked)
POST /api/services/cliproxy/auto-start Переключить автоматический запуск

Общий эндпоинт GET /api/services/{name}/logs (см. §4.1) работает для всех четырёх сервисов, используя динамический сегмент [name].


Mux имеет ту же структуру эндпоинтов, что и CLIProxyAPI, — маршрут rotate-key в API отсутствует (bearer-токен генерируется так же, как у 9Router: через getOrCreateApiKey("mux"), и внедряется посредством переменной окружения MUX_SERVER_AUTH_TOKEN, но отдельного эндпоинта для ротации пока нет). Mux управляется только на уровне жизненного цикла: в отличие от 9Router, у него нет исполнителя уровня 4, и он никогда не регистрируется как провайдер маршрутизации.

Метод Путь Описание
POST /api/services/mux/install Установить Mux из npm (npm i mux)
POST /api/services/mux/start Запустить Mux (mux server)
POST /api/services/mux/stop Остановить Mux
POST /api/services/mux/restart Перезапустить Mux
POST /api/services/mux/update Обновить до более новой версии npm
GET /api/services/mux/status Текущее состояние + состояние в БД
POST /api/services/mux/auto-start Переключить автоматический запуск

Bifrost — это ретрансляционный бэкенд ИИ-шлюза на Go (@maximhq/bifrost). Он использует ту же структуру эндпоинтов, что и CLIProxyAPI (без rotate-key — Bifrost управляет собственными ключами провайдеров в config.json в каталоге, указанном через -app-dir).

Метод Путь Описание
POST /api/services/bifrost/install Установить Bifrost из npm (@maximhq/bifrost)
POST /api/services/bifrost/start Запустить Bifrost на порту 8080 (по умолчанию)
POST /api/services/bifrost/stop Остановить Bifrost
POST /api/services/bifrost/restart Перезапустить Bifrost
POST /api/services/bifrost/update Обновить до более новой версии
GET /api/services/bifrost/status Текущее состояние + состояние в БД
POST /api/services/bifrost/auto-start Переключить автоматический запуск
GET /api/services/bifrost/logs Поток логов SSE (через общий динамический маршрут [name]/logs)

Подключение к маршрутизации: если BIFROST_BASE_URL не задан и управляемый экземпляр Bifrost запущен, getBifrostRoutingConfig() (в routingBackend.ts) автоматически использует http://127.0.0.1:{port} в качестве базового URL ретранслятора. Явно заданная переменная окружения BIFROST_BASE_URL всегда имеет приоритет.


Та же структура управления жизненным циклом, что и у других сервисов (install, start, stop, restart, update, status, auto-start, auto-restart-adopted), а также защищённая токеном плоскость управления OAuth в admin/: admin/accounts, admin/import-from-omniroute, admin/login-start, admin/login-complete (все защищены с помощью DARIO_ADMIN_TOKEN).

open-wa (@open-wa/wa-automate) управляет безголовым экземпляром Chromium (через Puppeteer) для автоматизации WhatsApp Web. Он использует ту же структуру эндпоинтов, что и Mux (маршрута rotate-key пока нет). Он управляется только на уровне жизненного цикла — не является целью маршрутизации, не имеет исполнителя уровня 4 или записи провайдера.

Метод Путь Описание
POST /api/services/openwa/install Установить open-wa из npm (@open-wa/wa-automate)
POST /api/services/openwa/start Запустить open-wa на порту 8323 (по умолчанию)
POST /api/services/openwa/stop Остановить open-wa
POST /api/services/openwa/restart Перезапустить open-wa
POST /api/services/openwa/update Обновить до более новой версии
GET /api/services/openwa/status Текущий статус + статус БД
POST /api/services/openwa/auto-start Переключить автоматический запуск
GET /api/services/openwa/logs Поток логов SSE (через общий динамический маршрут [name]/logs)

Ключ API: передаётся как WA_KEY — общее переопределение переменной окружения open-wa с префиксом WA_* сопоставляет её с параметром CLI --key/-k (dist/cli/setup.js::envArgs(), проверено на установленном пакете версии 4.76.0). При создании с помощью generateServiceApiKey() добавляется префикс ow_. open-wa считывает ключ из HTTP-заголовка key/api_key (а не Authorization: Bearer); /api-docs* явно освобождён от этой проверки (setupAuthenticationLayer в dist/cli/server.js), поэтому для проверки работоспособности заголовок аутентификации не требуется.

Сопряжение: open-wa является неофициальным решением и не связан с WhatsApp — для подключённого номера существует риск блокировки из-за собственных механизмов WhatsApp по обнаружению автоматизации. При первом запуске QR-код для сопряжения выводится в stdout и отображается через существующую панель логов/поток SSE — отдельной конечной точки для изображения QR-кода в этой интеграции пока нет.


4.7 Обратный прокси-сервер (встраивание панели управления 9Router)

Заголовок раздела «4.7 Обратный прокси-сервер (встраивание панели управления 9Router)»

Панель управления встраивает веб-интерфейс 9Router в iframe через внутренний обратный прокси-сервер по адресу:

GET|POST|... /dashboard/providers/services/9router/embed/[...path]

Этот прокси-сервер:

  • Перенаправляет запрос на http://127.0.0.1:{port}/{path} (только через loopback)
  • Удаляет входящие заголовки cookie и authorization (чтобы не допустить утечки сессии OmniRoute)
  • Добавляет Authorization: Bearer {apiKey} для аутентификации в 9Router
  • Удаляет из ответа set-cookie, content-security-policy, x-frame-options, cross-origin-*
  • Перезаписывает HTML-ответы, добавляя &lt;base href&gt; и нормализуя абсолютные пути (/foo → /dashboard/.../embed/foo)

Обновления WebSocket для встроенной панели управления обрабатываются вспомогательным сервером на выделенном порту (см. src/lib/services/embedWsProxy.ts).

Безопасность: маршруты встроенного прокси-сервера относятся к LOCAL_ONLY_API_PREFIXES и доступны только через loopback. Злоумышленник, получивший JWT через туннель Cloudflare/Ngrok, не сможет проксировать запросы во встроенные сервисы.


Все маршруты в /api/services/ и /dashboard/providers/services/*/embed/ классифицируются как LOCAL_ONLY в src/server/authz/routeGuard.ts. Проверка loopback-адреса выполняется безусловно перед любой ветвью аутентификации:

поступает запрос
→ isLocalOnlyPath(path)?
→ не loopback → 403 LOCAL_ONLY (всегда, до проверки аутентификации)
→ loopback → переход к обычной аутентификации

Это предотвращает использование скомпрометированного JWT (например, через туннель) для запуска npm install или создания процессов. Полную матрицу уровней см. в docs/security/ROUTE_GUARD_TIERS.md.

9Router и Mux требуют API-ключ или bearer-токен для собственных HTTP-эндпоинтов. OmniRoute:

  1. Генерирует ключ с помощью crypto.randomBytes(32).toString("base64url") с префиксом, специфичным для сервиса (nr_ для 9Router, mx_ для Mux).
  2. Шифрует его при хранении с помощью AES-256-GCM (того же шифра, который используется для учётных данных провайдера).
  3. Расшифровывает и внедряет его как переменную окружения при запуске процесса — NINEROUTER_API_KEY для 9Router, MUX_SERVER_AUTH_TOKEN для Mux (никогда не как флаг CLI, поэтому токен никогда не появляется в выводе ps или списках процессов).
  4. Никогда не возвращает ключ в открытом виде ни в одном HTTP-ответе.

CLIProxyAPI получает отдельный ключ плоскости данных, внедряемый при запуске (needsApiKey: true — используется для синхронизации моделей с адаптером).

Обратный HTTP-прокси (/dashboard/.../embed/[...path]) жёстко настроен на пересылку только на http://127.0.0.1:{port}. Он никогда не следует перенаправлениям к адресам, не относящимся к loopback. Библиотека ssrf-req-filter используется для отклонения любого URL вышестоящего сервера, который разрешается в адрес за пределами диапазона loopback.

Безопасность оболочки (жёсткое правило #13)

Заголовок раздела «Безопасность оболочки (жёсткое правило #13)»

npm install вызывается через execFile('npm', ['install', pkg, '--prefix', dir]) — без шаблонных литералов, оболочки и интерполяции внешних путей в строку команды. Значения времени выполнения (порты, API-ключи) передаются через объект env дочернего процесса.

Все ответы с ошибками из /api/services/* проходят через buildErrorBody() или sanitizeErrorMessage(). Необработанные err.stack и err.message никогда не возвращаются вызывающей стороне дословно.


Выполните следующие 8 шагов. Используйте существующие реализации в src/lib/services/installers/ и src/app/api/services/ как эталон.

Создайте src/lib/services/installers/{name}.ts по образцу ninerouter.ts:

export const NAME_PACKAGE = "your-npm-package";
export const NAME_DEFAULT_PORT = 20132; // выберите свободный порт
export async function install(version = "latest"): Promise<InstallResult> { ... }
export async function update(version = "latest"): Promise<InstallResult> { ... }
export async function uninstall(): Promise&lt;void&gt; { ... }
export function resolveSpawnArgs(apiKey: string, port: number): SpawnArgs { ... }
export async function getInstalledVersion(): Promise<string | null> { ... }
export async function getLatestVersion(): Promise<string | null> { ... }

Используйте runNpm(['install', NAME_PACKAGE, '--prefix', dir]) из installers/utils.ts — никогда не используйте execSync или интерполяцию оболочки.

Добавьте ServiceEntry в массив SERVICES в src/lib/services/bootstrap.ts:

{
tool: "myservice",
port: NAME_DEFAULT_PORT,
healthPath: "/health",
healthIntervalMs: 5_000,
stopTimeoutMs: 15_000,
logsBufferBytes: 5_242_880,
needsApiKey: true, // false, если API-ключ не требуется
}

Расширьте buildSpawnArgsFactory() для обработки cfg.tool === "myservice".

Подключаемый контракт плагина провайдера (этап 1, #7333)

Заголовок раздела «Подключаемый контракт плагина провайдера (этап 1, #7333)»

src/lib/services/providerPlugins/ вводит контракт ServiceProviderPlugin, который объединяет поля ServiceEntry из bootstrap.ts бэкенда и поля шаблона манифеста из serviceBackends.ts в одном объекте вместо раздельного описания структуры одного и того же бэкенда в двух не связанных между собой файлах. На момент написания мигрирован только 9router — bootstrap.ts формирует свою запись SERVICES[] из getServiceProviderPlugin("9router") (src/lib/services/providerPlugins/registry.ts) и выдаёт ошибку запуска, если плагин отсутствует. cliproxy, mux и bifrost остаются без изменений в виде ранее существовавших встроенных литералов SERVICES[].

В open-sse/config/providerPluginManifest.ts также был добавлен вспомогательный метод createServiceBackendManifestEntry(pluginId, template), который создаёт корректно сформированную запись ProviderPluginManifestEntry из записи SERVICE_BACKEND_MANIFEST_TEMPLATE — он ещё не подключён ни к одному действующему пути обработки запросов (ни к generateProviderPluginManifestFromRegistry(), ни к /v1/providers/[provider]/models); это останется последующей задачей после того, как контракт будет проверен на втором бэкенде.

В последующие PR, отслеживаемые в задаче #7333, отложены: миграция cliproxyapi через тот же реестр, обобщение mux/bifrost в объединение ServiceBackendPluginId, перенос особой логики маршрутизации исполнителей (open-sse/executors/index.ts, open-sse/handlers/chatCore/executorProxy.ts) в контракт плагина и подключение createServiceBackendManifestEntry() к действующему пути кода манифеста/моделей.

Шаг 3 — Добавление миграции и начальных данных БД

Заголовок раздела «Шаг 3 — Добавление миграции и начальных данных БД»

Убедитесь, что для сервиса существует строка в version_manager, добавив миграцию в src/lib/db/migrations/. Строка должна иметь следующий вид:

INSERT OR IGNORE INTO version_manager (tool, status, auto_start, provider_expose)
VALUES ('myservice', 'not_installed', 0, 0);

В каталоге src/app/api/services/{name}/:

_lib.ts вспомогательная функция getOrInitSupervisor()
install/route.ts POST — вызывает installer.install()
start/route.ts POST — вызывает supervisor.start()
stop/route.ts POST — вызывает supervisor.stop()
restart/route.ts POST — вызывает supervisor.restart()
update/route.ts POST — вызывает installer.update()
status/route.ts GET — объединяет текущее состояние и состояние БД
auto-start/route.ts POST — переключает флаг auto_start

Общий маршрут GET /api/services/[name]/logs уже подключён — изменения там не требуются.

Делегируйте формирование всех ответов об ошибках через createErrorResponse() / buildErrorBody().

В src/server/authz/routeGuard.ts убедитесь, что /api/services/ уже присутствует в списке. Если вы добавляете новый префикс (например, /api/tools/), добавьте его как в LOCAL_ONLY_API_PREFIXES, так и в SPAWN_CAPABLE_PREFIXES, если он запускает процессы. Добавьте тест в tests/unit/authz/routeGuard.test.ts.

Создайте src/app/(dashboard)/dashboard/providers/services/tabs/{Name}ServiceTab.tsx. Повторно используйте общие компоненты:

  • ServiceStatusCard — текущее состояние + индикатор работоспособности
  • ServiceLifecycleButtons — Запустить / Остановить / Перезапустить / Обновить
  • ServiceLogsPanel — просмотр последних журналов через SSE (подключается к /api/services/{name}/logs)
  • ApiKeyCard — отображение + ротация ключа (если needsApiKey: true)

Зарегистрируйте вкладку в ServicesPageShell.tsx.

Шаг 7 — Добавление записи провайдера (если сервис является целью маршрутизации)

Заголовок раздела «Шаг 7 — Добавление записи провайдера (если сервис является целью маршрутизации)»

Если встроенный сервис предоставляет OpenAI-совместимую конечную точку /v1/chat/completions:

  1. Добавьте запись провайдера в src/shared/constants/providers.ts с isEmbeddedService: true.
  2. Создайте open-sse/executors/{name}.ts, расширяющий BaseExecutor. При каждом запросе заново получайте порт и API-ключ (никогда не кэшируйте их в конструкторе). Возвращайте ответ 503 service_not_running, если состояние супервизора не равно "running".
  3. Зарегистрируйте модели в open-sse/config/providerRegistry.ts с префиксом сервиса (например, myservice/sub/model). modelSync.ts будет поддерживать их в актуальном состоянии.
  1. Обновите docs/frameworks/EMBEDDED-SERVICES.md (этот файл) — добавьте сервис в таблицу в §1, а все новые конечные точки — в §4.
  2. Добавьте модульные тесты в tests/unit/services/ (жизненный цикл, установщик, форма API).
  3. Добавьте интеграционный тест в tests/integration/services/ (активируется через RUN_SERVICES_INT=1).
  4. Обновите docs/openapi.yaml, добавив новые конечные точки.

Симптомы: Кнопка запуска возвращает 503, состояние остаётся "error" или "starting".

Проверка:

  1. Проверьте GET /api/services/{name}/logs (или панель Logs на информационной панели). Найдите строки вроде Error: ENOENT, address already in use или Cannot find module.
  2. Убедитесь, что npm находится в PATH: выполните which npm от имени той же учётной записи пользователя, под которой работает OmniRoute.
  3. Убедитесь, что сервис установлен: проверьте installedVersion в ответе GET /api/services/{name}/status. Если значение равно null, сначала выполните установку.
  4. Убедитесь, что каталог DATA_DIR/services/{name}/node_modules/ существует и не пуст.
  5. Проверьте поле lastError в ответе со статусом: оно содержит очищенное описание причины завершения.

Холодный запуск выполняется медленно (> 10 с до перехода в состояние running)

Заголовок раздела «Холодный запуск выполняется медленно (> 10 с до перехода в состояние running)»

Симптомы: Состояние долго остаётся "starting", прежде чем перейти в "running" или "error".

Объяснение: Холодный запуск 9Router включает импорт больших деревьев зависимостей (модули DNS, туннелирования и MITM). По умолчанию интервал проверки работоспособности составляет 2 с, а количество попыток — 3, после чего супервизор объявляет о превышении времени ожидания (но продолжает опрос).

Решение: Параметры healthIntervalMs и время ожидания waitForHealthy (healthIntervalMs * 3) настраиваются в bootstrap.ts. Для сервисов с более длительным временем запуска увеличьте healthIntervalMs до 5000, а stopTimeoutMs — до 30 000.


Симптомы: В журналах отображается address already in use :::20130.

Причины:

  • Другой процесс уже использует порт 20130.
  • Предыдущий процесс 9Router не был полностью остановлен (PID-процесс-зомби).

Решение:

  1. Измените порт по умолчанию с помощью переменной окружения NINEROUTER_PORT в .env.
  2. Найдите и завершите конфликтующий процесс: lsof -ti :20130 | xargs kill -9.
  3. Порт настраивается отдельно для каждого сервиса в bootstrap.ts с помощью поля port.

Примечание: 9Router по умолчанию использует порт 20130 специально, чтобы избежать конфликта с портом OmniRoute по умолчанию — 20128.


Симптомы: Установка возвращает 500, в журналах отображается EACCES или permission denied.

Причины:

  • DATA_DIR или его родительский каталог недоступен для записи процессу OmniRoute.
  • Запуск в Docker без прав root и без доступа на запись в подключённый том.

Решение:

  1. Проверьте DATA_DIR (по умолчанию: ~/.omniroute/): ls -la ~/.omniroute/
  2. Убедитесь, что каталог принадлежит пользователю, от имени которого запущен процесс OmniRoute: chown -R $USER ~/.omniroute/
  3. В Docker убедитесь, что подключённый том имеет правильные разрешения для пользователя контейнера.

Не удаётся выполнить обновление (превышение времени ожидания npm install или ошибка сети)

Заголовок раздела «Не удаётся выполнить обновление (превышение времени ожидания npm install или ошибка сети)»

Симптомы: Обновление возвращает 500 с InstallError, в журналах отображается сообщение о превышении времени ожидания сети.

Проверка:

  1. Убедитесь, что реестр npm доступен: npm ping.
  2. Проверьте настройки корпоративного прокси-сервера: npm config get proxy, npm config get https-proxy.
  3. Попробуйте выполнить установку вручную: npm install {package}@latest --prefix ~/.omniroute/services/{name}/.
  4. При работе в изолированной сети предварительно загрузите tarball-архив и выполните npm install /path/to/tarball.tgz.

Сразу после запуска сервис переходит в состояние "error" (быстрое аварийное завершение)

Заголовок раздела «Сразу после запуска сервис переходит в состояние "error" (быстрое аварийное завершение)»

Симптомы: Состояние переходит из "starting" в "error" менее чем за 5 секунд. В lastError отображается "Fast crash (exited with code 1)".

Проверка:

  1. Просмотрите полный конец журнала: GET /api/services/{name}/logs?tail=500.
  2. Распространённая причина: отсутствуют переменные окружения, необходимые сервису.
  3. Для 9Router: убедитесь, что NINEROUTER_DISABLE_MITM=true и NINEROUTER_DISABLE_TUNNEL=true присутствуют в окружении, передаваемом при создании процесса (см. resolveSpawnArgs в installers/ninerouter.ts).

В: Можно ли открыть эндпоинты встроенных сервисов для клиентов, подключающихся не через loopback-интерфейс?

Нет. Уровень LOCAL_ONLY задан намеренно (жёсткое правило №17). Маршруты, которые могут запускать npm install или создавать процессы node, не должны быть доступны для трафика не через loopback-интерфейс, поскольку утечка JWT через туннель (Cloudflare, Ngrok, Tailscale) в противном случае позволила бы произвольно создавать процессы. Для /api/services/ нет исключения, позволяющего отказаться от этого ограничения: в отличие от /api/mcp/, этот маршрут исключён из списка обхода проверки области manage. См. docs/security/ROUTE_GUARD_TIERS.md.


В: Будут ли 9Router и CLIProxyAPI доступны в производственных/облачных развёртываниях?

Да. Оба сервиса следуют той же локально-ориентированной модели, что и сам OmniRoute. Они работают на одном компьютере и взаимодействуют через loopback-интерфейс. Под «производственной средой» здесь понимается VPS или локальный сервер, на котором развёрнут OmniRoute, а не удалённый облачный провайдер.


В: Как выполнить отладку супервизора?

  1. Просматривайте поток журналов SSE: curl -N http://localhost:20128/api/services/9router/logs.
  2. Проверьте структурированные журналы в выводе pino OmniRoute, отфильтрованные по пространству имён service:supervisor.
  3. Проверьте строку в БД: sqlite3 ~/.omniroute/omniroute.db "SELECT * FROM version_manager WHERE tool='9router'".
  4. Используйте GET /api/services/9router/status, чтобы одним запросом получить текущее фактическое состояние, PID, статус работоспособности и lastError.

В: Супервизор показывает health: "degraded" или health: "unknown", но состояние — "running". Это проблема?

"degraded" означает, что проверка работоспособности вернула ответ с кодом, отличным от 200. "unknown" означает, что ни одна проверка ещё не завершилась (состояние гонки с первым опросом). Во время запуска оба состояния являются временными. Если состояние работоспособности остаётся "degraded" более healthIntervalMs * 3 мс после перехода в состояние "running", встроенный сервис запущен, но его HTTP API не отвечает. Проверьте, правильно ли указан порт в ответе о состоянии и действительно ли сервис прослушивает этот порт.


В: Можно ли изменить API-ключ 9Router без полного перезапуска?

Нет. API-ключ передаётся 9Router через переменную окружения во время создания процесса. Переменные окружения невозможно изменить в уже запущенном процессе. POST .../rotate-key автоматически останавливает и перезапускает сервис, чтобы применить новый ключ. Ротация ключа вступает в силу в течение заданного для сервиса stopTimeoutMs (по умолчанию 15 с) плюс время, необходимое для его запуска.


В: Каков предельный размер кольцевого буфера и что происходит при его заполнении?

Для каждого сервиса выделяется отдельный кольцевой буфер размером 5 МБ. Когда буфер заполняется, самые старые строки журнала удаляются, чтобы освободить место для новых. Событие SSE snapshot возвращает самые последние строки в пределах ограничения tail. Журналы не сохраняются на диск, если только в строке БД не задан logsBufferPath.


  • docs/security/ROUTE_GUARD_TIERS.md — сведения об уровне LOCAL_ONLY
  • docs/architecture/CODEBASE_DOCUMENTATION.md — §3.2 Схема модуля встроенных сервисов
  • docs/architecture/ARCHITECTURE.md — контекст на уровне системы
  • docs/openapi.yaml — машиночитаемые определения эндпоинтов
  • CLAUDE.md §«Добавление нового встроенного сервиса» — краткий контрольный список

Исходный код OmniRoute (a58000c7685f)

HagiCode

HagiCode — агентная среда разработки со структурированными процессами, параллельным выполнением несколькими агентами и интерфейсами Hero Dungeon.

Превращайте идеи в полезное ПО с более умным, быстрым и увлекательным агентным рабочим процессом.

Главный экран HagiCode в светлой теме
  • SmartСтруктурированные процессы превращают намерение в исполнимый путь от идеи до готового изменения.
  • EfficientМультиагентные процессы параллельно продвигают исследование, реализацию и проверку.
  • FunHero Dungeon делает длительную совместную разработку наглядной и увлекательной.
Перейти на HagiCode