Руководство по интеграции через REST API
Для команд, встраивающих Qwen Code в собственный продукт поверх HTTP: запустите qwen serve
как бэкенд и управляйте им из собственного фронтенда.
Эта страница — отправная точка. Полный справочник маршрутов находится в
qwen-serve-protocol.md; внутреннее устройство описано в
углублённом руководстве по демону; рабочий пример на TypeScript —
в examples/daemon-client-quickstart.md.
Какие пути существуют
Шесть способов построения на базе демона, разделённых одним вопросом — какую часть фронтенда вы контролируете?
| Путь | Вы контролируете | Статус |
|---|---|---|
| демон + встроенный Web Shell | ничего — используете как есть | доступен сейчас (руководство пользователя) |
демон --no-web + ваш собственный UI | весь фронтенд целиком | доступен сейчас — эта страница |
| демон + брендированный Web Shell | брендинг, а не код | не реализовано (#11357 ) |
| демон + самостоятельно собранный Web Shell | сборку фронтенда | не реализовано (#11358 ) |
демон через SDK DaemonClient | клиентский код, без сырого HTTP | доступен сейчас (TS, Java) — Python SDK работает только с процесс-транспортом и не имеет клиента демона, поэтому Python-интеграция использует путь 2 через сырой HTTP |
| демон через MCP bridge | ничего — другой агент управляет им | поставляется как qwen-serve-mcp в @qwen-code/sdk — см. README бриджа; QWEN_BRIDGE_ALLOW_GLOBAL_SCOPE опционально разрешает мутации в глобальной области |
Headless qwen -p и ACP через stdio для редакторов — это отдельные пути интеграции.
Каналы и расширения также могут работать через демон; см.
руководство по каналам и
справочник расширений.
Что нужно знать перед проектированием
Демон не выполняет инференс внутри своего процесса. Он порождает дочерние процессы
qwen --acp и выступает посредником между ними и HTTP. Он запускает входной скрипт CLI
под тем же бинарём Node, используя QWEN_CLI_ENTRY или, иначе, process.argv[1].
Встраиваемый Node-бэкенд должен указывать в QWEN_CLI_ENTRY путь к установленному
входному скрипту Qwen CLI; поиск qwen в PATH не производится. Отсутствие точки входа
проявляется как MissingCliEntryError.
В установившемся режиме существует один дочерний процесс на активное рабочее пространство,
а не по одному на сессию. Каждая сессия в рабочем пространстве мультиплексируется на этот
дочерний процесс и разделяет его процесс, состояние OAuth, файловый кэш и разбор иерархической
памяти. Таким образом, домен отказа — это рабочее пространство: если дочерний процесс
завершается, все мультиплексированные на него сессии уничтожаются вместе. Рассчитывайте
контейнер на демон плюс один дочерний процесс на зарегистрированное рабочее пространство,
с запасом на один дополнительный дочерний процесс на среду выполнения при переключении канала.
Когда сессии должны отказывать независимо, запускайте отдельные демоны —
--max-sessions ограничивает параллелизм, а не радиус поражения.
Аутентификация рассчитана на одного оператора. Runtime bearer-токен предоставляет доступ ко всему bearer-защищённому API, а доверенный loopback-вызывающий получает полные полномочия, включая выполнение кода от имени пользователя демона. Модели принципала для каждого конечного пользователя нет. Если вы размещаете это за мультипользовательским продуктом, ваш бэкенд отвечает за идентификацию пользователей и не должен передавать токен демона в браузеры. Контейнеризированное и мультитенантное развёртывание явно отложено — см. «v0.16-alpha known limits» в руководстве пользователя.
Настроенный вход webhook канала (POST /channels/:channelName/webhooks/:source)
использует собственную аутентификацию x-qwen-webhook-secret до bearer-аутентификации;
он неактивен, пока не настроен источник webhook канала.
Запуск демона
export QWEN_SERVER_TOKEN="$(openssl rand -hex 32)"
qwen serve --no-web --require-auth \
--hostname 0.0.0.0 --port 4170 \
--workspace /srv/project--no-web сохраняет перечисленные ниже маршруты, но отключает ресурсы Web Shell и
зависимые поверхности: на macOS — маршруты /live/* и сокет /live/host, а на
каждой платформе — GET /mcp-app-sandbox. Передавайте токен через переменную окружения,
а не через --token, который доступен любому локальному пользователю через /proc/<pid>/cmdline.
Примеры Bash ниже передают заголовок Authorization через файловый дескриптор
с помощью встроенной команды printf оболочки, не включая токен в аргументы curl.
Маршруты, которые реально использует интеграция
Большая часть того, что регистрирует демон, существует для работы Web Shell — операции git, установка расширений, доверие к рабочему пространству, голос, запланированные задачи — и меняется вместе с этим UI. Нижеприведённое подмножество на порядок меньше.
Это то, что нужно REST-интеграции. Остальное считайте внутренним.
Обнаружение
| Маршрут | Назначение |
|---|---|
GET /health | Liveness-проба |
GET /capabilities | Предварительный запрос — прочитайте workspaceCwd и policy.permission до всего остального |
Жизненный цикл сессии
| Маршрут | Назначение |
|---|---|
POST /session | Создание. Отправьте sessionScope: "thread" для независимого разговора |
DELETE /session/:id | Закрытие. Сохранённая сессия остаётся и может быть перезагружена |
POST /session/:id/load · /resume | Восстановление сохранённой сессии |
POST /session/:id/heartbeat | Отсрочка idle-утилизатора |
PATCH /session/:id/metadata | Метаданные сессии |
POST /session/:id/model | Переключение модели в пределах привязанной службы |
GET /session/:id/status | Статус среды выполнения — отдельного раздела справочника пока нет |
Промптинг и потоковая передача
| Маршрут | Назначение |
|---|---|
POST /session/:id/prompt | Отправка. Возвращает 202 при допуске, а не при завершении |
POST /session/:id/cancel | Отмена только активного промпта |
GET /session/:id/events | Поток SSE. Подписывайтесь до отправки промпта |
GET /session/:id/transcript | История разговора |
GET /session/:id/context | Использование окна контекста |
GET /session/:id/export · GET /session/:id/pending-prompts | Отдельных разделов справочника пока нет |
Разрешения
| Маршрут | Назначение |
|---|---|
POST /session/:id/permission/:requestId | Ответ на permission_request. Маршрутизируется к среде выполнения, владеющей сессией, поэтому работает корректно при любой конфигурации рабочего пространства — отдельного раздела пока нет |
POST /permission/:requestId | Процесс-глобальная форма, подключённая только к бриджу основного рабочего пространства: возвращает 404 для сессии, принадлежащей другой зарегистрированной среде выполнения, с тем же телом, что и пропавший голос при политике first-responder по умолчанию — поэтому 404 здесь сам по себе не означает, что запрос уже был отвечен |
Контекст рабочего пространства только для чтения
| Маршрут | Назначение |
|---|---|
GET /file · /file/bytes | Чтение файла или диапазона байт |
GET /stat · GET /list · GET /glob | Метаданные пути, список каталога, glob — отдельных разделов пока нет |
GET /workspace/tools | Инструменты, сообщённые активным дочерним ACP-процессом; без него ответ содержит acpChannelLive: false, tools: [] и ошибку not_started — отдельного раздела пока нет |
Покрытие справочника. 17 из 25 вышеперечисленных маршрутов имеют отдельные разделы. Из 8 отмеченных иначе некоторые упомянуты лишь вскользь, а три отсутствуют полностью:
GET /session/:id/pending-prompts,POST /session/:id/permission/:requestIdиGET /workspace/tools. Закрытие этого пробела отслеживается в #11359 .
Минимальный поток
1. Предварительный запрос. Прочитайте workspaceCwd (чтобы не передавать cwd при создании) и
policy.permission (чтобы знать, кто может отвечать на запросы разрешений).
curl -sH @<(printf 'Authorization: Bearer %s\n' "$QWEN_SERVER_TOKEN") http://daemon:4170/capabilities2. Создание сессии. Используйте sessionScope: "thread", если вызывающие не должны
разделять один разговор — значение по умолчанию "single" заставляет повторное создание
в том же рабочем пространстве переиспользовать существующую сессию, сериализуя
независимых вызывающих через одну очередь.
curl -sX POST http://daemon:4170/session \
-H @<(printf 'Authorization: Bearer %s\n' "$QWEN_SERVER_TOKEN") -H 'Content-Type: application/json' \
-d '{"sessionScope":"thread"}'
# → {"sessionId":"…","workspaceCwd":"/srv/project","attached":false}3. Подписка до отправки промпта. Last-Event-ID: 0 воспроизводит с самого старого
сохранённого события — так вы ловите события, возникшие между созданием и подпиской,
в частности model_switch_failed. При подключении (значение по умолчанию
sessionScope: "single" переиспользует существующую сессию) это событие — единственный
сигнал того, что некорректный modelServiceId был отклонён, потому что ошибка
намеренно не передаётся как HTTP-ошибка. При новом создании с modelServiceId —
чего тело шага 2 не содержит — тело 200 также содержит modelApplied, false если
переключение было отклонено, и это детерминированный сигнал для действия, в отличие от
события на кольце с ограниченным буфером. Создание без modelServiceId вообще не имеет
ключа modelApplied.
curl -N http://daemon:4170/session/$SID/events \
-H @<(printf 'Authorization: Bearer %s\n' "$QWEN_SERVER_TOKEN") \
-H 'Accept: text/event-stream' -H 'Last-Event-ID: 0'Каждая строка data: — это полный Envelope на одной строке; type Envelope совпадает
со строкой event:.
Воспроизведение ограничено --event-ring-size и фиксированным байтовым бюджетом 8 МиБ
на подписку. Если поток выдаёт state_resync_required с
reason: "replay_budget_exceeded", восстановитесь через POST /session/:id/load,
а не считайте воспроизведение завершённым.
4. Промптинг. 202 означает допуск, а не завершение. Соотнесите turn_complete /
turn_error в потоке по promptId. Читайте stopReason в turn_complete;
в turn_error читайте message и необязательные code / errorKind — см.
POST /session/:id/prompt.
curl -sX POST http://daemon:4170/session/$SID/prompt \
-H @<(printf 'Authorization: Bearer %s\n' "$QWEN_SERVER_TOKEN") -H 'Content-Type: application/json' \
-d '{"prompt":[{"type":"text","text":"What does src/main.ts do?"}]}'
# → 202 {"promptId":"…","lastEventId":42}5. Ответы на запросы разрешений. Когда агент хочет выполнить инструмент и его
режим одобрения требует подтверждения, он выдаёт permission_request, и ход
блокируется, пока кто-то не ответит или вы не отмените — по умолчанию таймаута нет
(--permission-response-timeout-ms по умолчанию 0 = ждать бесконечно), поэтому
неотвеченный запрос продолжает занимать слот в очереди промптов сессии, пока вы
не отмените или не закроете сессию. Установите собственный дедлайн, если потоку он нужен.
Режим — это собственная настройка Qwen дочернего процесса tools.approvalMode,
разрешаемая из настроек хоста демона и каталога --workspace; демон ничего
не фиксирует при запуске. По умолчанию auto, который одобряет один класс вызовов
инструментов без спрашивания — те не выдают permission_request вообще — и продолжает
спрашивать для остальных. Недоверенная папка рабочего пространства принудительно
переключается на default (спрашивать), поэтому одна инсталляция видит эти события,
а другая нет, и GET /capabilities сообщает политику посредничества голосов, а не
режим одобрения, так что предварительный запрос не скажет, в какой позиции вы находитесь.
Если ваша интеграция зависит от гейтинга одобрения, зафиксируйте tools.approvalMode
явно и решите заранее, как он отвечает: автоодобрение уже может действовать без того,
чтобы кто-либо его выбирал.
Отвечайте на маршруте сессии: он маршрутизируется к среде выполнения, владеющей сессией, поэтому работает при любой конфигурации рабочего пространства.
curl -sX POST http://daemon:4170/session/$SID/permission/$REQUEST_ID \
-H @<(printf 'Authorization: Bearer %s\n' "$QWEN_SERVER_TOKEN") -H 'Content-Type: application/json' \
-d '{"outcome":{"outcome":"selected","optionId":"proceed_once"}}'6. Закрытие. DELETE /session/$SID → 204. Сессия на диске сохраняется.
Эксплуатация
| Забота | Где |
|---|---|
| Ограничения параллелизма | --max-sessions, --max-total-sessions; создания сверх лимита возвращают 503 с Retry-After |
| Ограничение частоты | --rate-limit и флаги --rate-limit-* для каждого класса |
| Очистка неактивных | --session-idle-timeout-ms; поддержка активности через POST /session/:id/heartbeat |
| Память | --child-heap-mode — только наблюдение. --memory-budget-mb управляет адаптивным пулом роста live-журнала для POST /session/:id/load, а не для воспроизведения SSE; фиксация --max-journal-bytes или --max-journal-events отключает рост. Ни один флаг не определяет размер дочерних процессов и не отказывает в запуске, и не управляет их фактическим потолком кучи (--max-old-space-size, производный от памяти хоста). См. Конфигурация для расчёта бюджета. Воспроизведение SSE отдельно ограничено --event-ring-size и фиксированным бюджетом 8 МиБ на подписку; пропущенный хвост выдаёт state_resync_required с reason: "replay_budget_exceeded" |
| Дедлайны промптов | --prompt-deadline-ms; истечение выдаёт turn_error |
| Ошибки | Классификация ошибок |
| Наблюдаемость | Наблюдаемость |
| Полный список флагов | Конфигурация |