Embedded Services (Русский)
Содержание
Заголовок раздела «Содержание»- Обзор
- Архитектура — 4 уровня
- Конечный автомат жизненного цикла
- Справочник API
- Безопасность
- Добавление нового встроенного сервиса
- Устранение неполадок
- Часто задаваемые вопросы
1. Обзор
Заголовок раздела «1. Обзор»Зачем нужны встроенные сервисы?
Заголовок раздела «Зачем нужны встроенные сервисы?»Встроено шесть сервисов:
| Сервис | 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.jsonOmniRoute) - 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 (три вкладки) |
| Автозапуск | Переключатель для каждого сервиса, по умолчанию выключен |
2. Архитектура — 4 слоя
Заголовок раздела «2. Архитектура — 4 слоя»┌────────────────────────────────────────────────────────────────────┐│ Слой 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) |
3. Конечный автомат жизненного цикла
Заголовок раздела «3. Конечный автомат жизненного цикла» 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() для одного
супервизора приводят к запуску ровно одного процесса; второй вызывающий ожидает
и получает существующий статус. Это предотвращает состояния гонки, например,
когда автозапуск и кнопка в пользовательском интерфейсе срабатывают одновременно.
4. Справочник API
Заголовок раздела «4. Справочник API»Все маршруты в /api/services/ доступны LOCAL_ONLY (только через loopback, жёсткое правило #17).
Запросы не через loopback получают 403 LOCAL_ONLY независимо от токена аутентификации.
4.1 Эндпоинты 9Router (11 маршрутов)
Заголовок раздела «4.1 Эндпоинты 9Router (11 маршрутов)»POST /api/services/9router/install
Заголовок раздела «POST /api/services/9router/install»Устанавливает 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 возвращаются в виде понятных сообщений.
POST /api/services/9router/start
Заголовок раздела «POST /api/services/9router/start»Запускает 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}POST /api/services/9router/stop
Заголовок раздела «POST /api/services/9router/stop»Корректно останавливает 9Router. Отправляет SIGTERM, ожидает 15 с, а затем отправляет SIGKILL, если процесс всё ещё работает. Идемпотентно, если сервис уже остановлен.
Тело запроса: отсутствует
Ответы:
| Статус | Описание |
|---|---|
200 |
ServiceStatus (state: “stopped”) |
503 |
Непредвиденный сбой остановки |
POST /api/services/9router/restart
Заголовок раздела «POST /api/services/9router/restart»Эквивалентно последовательному вызову stop() и start() под блокировкой операции.
Тело запроса: отсутствует
Ответы: такие же, как у start (возвращает итоговый ServiceStatus).
POST /api/services/9router/update
Заголовок раздела «POST /api/services/9router/update»Обновляет 9Router до более новой версии npm. Если сервис запущен, сначала он останавливается, затем выполняется npm install (новая версия устанавливается на месте), после чего сервис перезапускается.
Тело запроса (все поля необязательны):
{ "version": "latest" }Ответы:
| Статус | Описание |
|---|---|
200 |
{ ok: true, previousVersion: "...", installedVersion: "..." } |
400 |
Недопустимое тело запроса |
500 |
Сбой обновления npm |
POST /api/services/9router/rotate-key
Заголовок раздела «POST /api/services/9router/rotate-key»Генерирует новый API-ключ для 9Router, шифрует его при хранении и перезапускает сервис (если он запущен), чтобы тот получил новый ключ из своего окружения. Старый ключ немедленно становится недействительным.
Тело запроса: отсутствует
Ответы:
| Статус | Описание |
|---|---|
200 |
{ keyRotated: true, restarted: boolean } |
500 |
Сбой ротации |
Безопасность: Новый ключ никогда не возвращается в ответе (утечки учётных данных нет).
Он хранится в зашифрованном виде (AES-256-GCM) в таблице version_manager.
GET /api/services/9router/status
Заголовок раздела «GET /api/services/9router/status»Возвращает объединённый актуальный статус и статус из БД, включая метаданные версии и предварительное представление 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}POST /api/services/9router/auto-start
Заголовок раздела «POST /api/services/9router/auto-start»Переключает флаг автоматического запуска. При enabled: true сервис автоматически
запустится при следующем запуске OmniRoute (если сервис установлен).
Тело запроса:
{ "enabled": true }Ответы:
| Статус | Описание |
|---|---|
200 |
{ autoStart: true } |
400 |
Недопустимое тело |
GET /api/services/9router/logs
Заголовок раздела «GET /api/services/9router/logs»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 |
Сервис не найден (супервизор не зарегистрирован) |
4.2 Эндпоинты CLIProxyAPI (10 маршрутов)
Заголовок раздела «4.2 Эндпоинты CLIProxyAPI (10 маршрутов)»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].
4.3 Эндпоинты Mux (8 маршрутов)
Заголовок раздела «4.3 Эндпоинты Mux (8 маршрутов)»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 |
Переключить автоматический запуск |
4.4 Эндпоинты Bifrost (8 маршрутов)
Заголовок раздела «4.4 Эндпоинты Bifrost (8 маршрутов)»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 всегда имеет приоритет.
4.5 Эндпоинты Dario (12 маршрутов)
Заголовок раздела «4.5 Эндпоинты Dario (12 маршрутов)»Та же структура управления жизненным циклом, что и у других сервисов (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).
4.6 Эндпоинты open-wa (7 маршрутов)
Заголовок раздела «4.6 Эндпоинты open-wa (7 маршрутов)»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-ответы, добавляя
<base href>и нормализуя абсолютные пути (/foo→/dashboard/.../embed/foo)
Обновления WebSocket для встроенной панели управления обрабатываются вспомогательным
сервером на выделенном порту (см. src/lib/services/embedWsProxy.ts).
Безопасность: маршруты встроенного прокси-сервера относятся к
LOCAL_ONLY_API_PREFIXES и доступны только через loopback. Злоумышленник,
получивший JWT через туннель Cloudflare/Ngrok, не сможет проксировать запросы
во встроенные сервисы.
5. Безопасность
Заголовок раздела «5. Безопасность»Применение LOCAL_ONLY (жёсткое правило #17)
Заголовок раздела «Применение LOCAL_ONLY (жёсткое правило #17)»Все маршруты в /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.
Внедрение API-ключа
Заголовок раздела «Внедрение API-ключа»9Router и Mux требуют API-ключ или bearer-токен для собственных HTTP-эндпоинтов. OmniRoute:
- Генерирует ключ с помощью
crypto.randomBytes(32).toString("base64url")с префиксом, специфичным для сервиса (nr_для 9Router,mx_для Mux). - Шифрует его при хранении с помощью AES-256-GCM (того же шифра, который используется для учётных данных провайдера).
- Расшифровывает и внедряет его как переменную окружения при запуске процесса —
NINEROUTER_API_KEYдля 9Router,MUX_SERVER_AUTH_TOKENдля Mux (никогда не как флаг CLI, поэтому токен никогда не появляется в выводеpsили списках процессов). - Никогда не возвращает ключ в открытом виде ни в одном HTTP-ответе.
CLIProxyAPI получает отдельный ключ плоскости данных, внедряемый при запуске
(needsApiKey: true — используется для синхронизации моделей с адаптером).
Защита от SSRF
Заголовок раздела «Защита от SSRF»Обратный 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
дочернего процесса.
Санитизация ошибок (жёсткое правило #12)
Заголовок раздела «Санитизация ошибок (жёсткое правило #12)»Все ответы с ошибками из /api/services/* проходят через buildErrorBody() или
sanitizeErrorMessage(). Необработанные err.stack и err.message никогда не
возвращаются вызывающей стороне дословно.
6. Добавление нового встроенного сервиса
Заголовок раздела «6. Добавление нового встроенного сервиса»Выполните следующие 8 шагов. Используйте существующие реализации в
src/lib/services/installers/ и src/app/api/services/ как эталон.
Шаг 1 — Создание установщика
Заголовок раздела «Шаг 1 — Создание установщика»Создайте 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<void> { ... }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 или интерполяцию оболочки.
Шаг 2 — Регистрация в bootstrap
Заголовок раздела «Шаг 2 — Регистрация в bootstrap»Добавьте 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);Шаг 4 — Создание 7 API-эндпоинтов
Заголовок раздела «Шаг 4 — Создание 7 API-эндпоинтов»В каталоге 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().
Шаг 5 — Добавление в LOCAL_ONLY_API_PREFIXES
Заголовок раздела «Шаг 5 — Добавление в LOCAL_ONLY_API_PREFIXES»В src/server/authz/routeGuard.ts убедитесь, что /api/services/ уже присутствует в списке.
Если вы добавляете новый префикс (например, /api/tools/), добавьте его как в
LOCAL_ONLY_API_PREFIXES, так и в SPAWN_CAPABLE_PREFIXES, если он запускает процессы.
Добавьте тест в tests/unit/authz/routeGuard.test.ts.
Шаг 6 — Добавление вкладки интерфейса
Заголовок раздела «Шаг 6 — Добавление вкладки интерфейса»Создайте 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:
- Добавьте запись провайдера в
src/shared/constants/providers.tsсisEmbeddedService: true. - Создайте
open-sse/executors/{name}.ts, расширяющийBaseExecutor. При каждом запросе заново получайте порт и API-ключ (никогда не кэшируйте их в конструкторе). Возвращайте ответ503 service_not_running, если состояние супервизора не равно"running". - Зарегистрируйте модели в
open-sse/config/providerRegistry.tsс префиксом сервиса (например,myservice/sub/model).modelSync.tsбудет поддерживать их в актуальном состоянии.
Шаг 8 — Документация и тестирование
Заголовок раздела «Шаг 8 — Документация и тестирование»- Обновите
docs/frameworks/EMBEDDED-SERVICES.md(этот файл) — добавьте сервис в таблицу в §1, а все новые конечные точки — в §4. - Добавьте модульные тесты в
tests/unit/services/(жизненный цикл, установщик, форма API). - Добавьте интеграционный тест в
tests/integration/services/(активируется черезRUN_SERVICES_INT=1). - Обновите
docs/openapi.yaml, добавив новые конечные точки.
7. Устранение неполадок
Заголовок раздела «7. Устранение неполадок»Сервис не запускается
Заголовок раздела «Сервис не запускается»Симптомы: Кнопка запуска возвращает 503, состояние остаётся "error" или "starting".
Проверка:
- Проверьте
GET /api/services/{name}/logs(или панель Logs на информационной панели). Найдите строки вродеError: ENOENT,address already in useилиCannot find module. - Убедитесь, что
npmнаходится в PATH: выполнитеwhich npmот имени той же учётной записи пользователя, под которой работает OmniRoute. - Убедитесь, что сервис установлен: проверьте
installedVersionв ответеGET /api/services/{name}/status. Если значение равноnull, сначала выполните установку. - Убедитесь, что каталог
DATA_DIR/services/{name}/node_modules/существует и не пуст. - Проверьте поле
lastErrorв ответе со статусом: оно содержит очищенное описание причины завершения.
Холодный запуск выполняется медленно (> 10 с до перехода в состояние running)
Заголовок раздела «Холодный запуск выполняется медленно (> 10 с до перехода в состояние running)»Симптомы: Состояние долго остаётся "starting", прежде чем перейти в "running" или "error".
Объяснение: Холодный запуск 9Router включает импорт больших деревьев зависимостей (модули DNS, туннелирования и MITM). По умолчанию интервал проверки работоспособности составляет 2 с, а количество попыток — 3, после чего супервизор объявляет о превышении времени ожидания (но продолжает опрос).
Решение: Параметры healthIntervalMs и время ожидания waitForHealthy
(healthIntervalMs * 3) настраиваются в bootstrap.ts. Для сервисов с более длительным
временем запуска увеличьте healthIntervalMs до 5000, а stopTimeoutMs — до 30 000.
Конфликт порта (EADDRINUSE)
Заголовок раздела «Конфликт порта (EADDRINUSE)»Симптомы: В журналах отображается address already in use :::20130.
Причины:
- Другой процесс уже использует порт 20130.
- Предыдущий процесс 9Router не был полностью остановлен (PID-процесс-зомби).
Решение:
- Измените порт по умолчанию с помощью переменной окружения
NINEROUTER_PORTв.env. - Найдите и завершите конфликтующий процесс:
lsof -ti :20130 | xargs kill -9. - Порт настраивается отдельно для каждого сервиса в
bootstrap.tsс помощью поляport.
Примечание: 9Router по умолчанию использует порт 20130 специально, чтобы избежать конфликта с портом OmniRoute по умолчанию — 20128.
Отказано в доступе (EACCES) при установке
Заголовок раздела «Отказано в доступе (EACCES) при установке»Симптомы: Установка возвращает 500, в журналах отображается EACCES или permission denied.
Причины:
DATA_DIRили его родительский каталог недоступен для записи процессу OmniRoute.- Запуск в Docker без прав root и без доступа на запись в подключённый том.
Решение:
- Проверьте
DATA_DIR(по умолчанию:~/.omniroute/):ls -la ~/.omniroute/ - Убедитесь, что каталог принадлежит пользователю, от имени которого запущен процесс OmniRoute:
chown -R $USER ~/.omniroute/ - В Docker убедитесь, что подключённый том имеет правильные разрешения для пользователя контейнера.
Не удаётся выполнить обновление (превышение времени ожидания npm install или ошибка сети)
Заголовок раздела «Не удаётся выполнить обновление (превышение времени ожидания npm install или ошибка сети)»Симптомы: Обновление возвращает 500 с InstallError, в журналах отображается сообщение о превышении времени ожидания сети.
Проверка:
- Убедитесь, что реестр npm доступен:
npm ping. - Проверьте настройки корпоративного прокси-сервера:
npm config get proxy,npm config get https-proxy. - Попробуйте выполнить установку вручную:
npm install {package}@latest --prefix ~/.omniroute/services/{name}/. - При работе в изолированной сети предварительно загрузите tarball-архив и выполните
npm install /path/to/tarball.tgz.
Сразу после запуска сервис переходит в состояние "error" (быстрое аварийное завершение)
Заголовок раздела «Сразу после запуска сервис переходит в состояние "error" (быстрое аварийное завершение)»Симптомы: Состояние переходит из "starting" в "error" менее чем за 5 секунд.
В lastError отображается "Fast crash (exited with code 1)".
Проверка:
- Просмотрите полный конец журнала:
GET /api/services/{name}/logs?tail=500. - Распространённая причина: отсутствуют переменные окружения, необходимые сервису.
- Для 9Router: убедитесь, что
NINEROUTER_DISABLE_MITM=trueиNINEROUTER_DISABLE_TUNNEL=trueприсутствуют в окружении, передаваемом при создании процесса (см.resolveSpawnArgsвinstallers/ninerouter.ts).
8. Часто задаваемые вопросы
Заголовок раздела «8. Часто задаваемые вопросы»В: Можно ли открыть эндпоинты встроенных сервисов для клиентов, подключающихся не через 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, а не удалённый облачный провайдер.
В: Как выполнить отладку супервизора?
- Просматривайте поток журналов SSE:
curl -N http://localhost:20128/api/services/9router/logs. - Проверьте структурированные журналы в выводе pino OmniRoute, отфильтрованные по
пространству имён
service:supervisor. - Проверьте строку в БД:
sqlite3 ~/.omniroute/omniroute.db "SELECT * FROM version_manager WHERE tool='9router'". - Используйте
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_ONLYdocs/architecture/CODEBASE_DOCUMENTATION.md— §3.2 Схема модуля встроенных сервисовdocs/architecture/ARCHITECTURE.md— контекст на уровне системыdocs/openapi.yaml— машиночитаемые определения эндпоинтовCLAUDE.md§«Добавление нового встроенного сервиса» — краткий контрольный список
HagiCode
HagiCode — агентная среда разработки со структурированными процессами, параллельным выполнением несколькими агентами и интерфейсами Hero Dungeon.
Превращайте идеи в полезное ПО с более умным, быстрым и увлекательным агентным рабочим процессом.

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