Руководство по интеграции через REST API
Для команд, встраивающих Qwen Code в собственный продукт поверх HTTP: запустите qwen serve
как бэкенд и управляйте им из собственного фронтенда.
Эта страница — отправная точка. Курируемый
справочник Daemon REST API описывает стабильную
поверхность интеграции и ссылается на контракт OpenAPI 3.1. Полный протокол находится в
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 канала.
Запуск демона
Сгенерируйте токен один раз в терминале 1. Встроенная команда оболочки выводит его, чтобы вы могли вставить то же значение в скрытый запрос, показанный ниже, в каждом остальном терминале:
export QWEN_SERVER_TOKEN="$(openssl rand -hex 32)"
printf 'Copy this token to the other terminals: %s\n' "$QWEN_SERVER_TOKEN"
export DAEMON_URL=http://127.0.0.1:4170Терминал 1 — эта команда блокирует, поэтому оставьте её работающей:
qwen serve --no-web --require-auth \
--hostname 0.0.0.0 --port 4170 \
--workspace /srv/projectВ каждом другом терминале вставьте токен, выведенный терминалом 1, когда read
запросит его. Это сохраняет токен вне истории оболочки и аргументов дочерних
процессов:
read -rsp 'QWEN_SERVER_TOKEN: ' QWEN_SERVER_TOKEN; printf '\n'
export QWEN_SERVER_TOKEN
export DAEMON_URL=http://127.0.0.1:4170DAEMON_URL — это loopback-базовый URL, который использует каждая клиентская
команда ниже — экспортируйте его с тем же значением в каждом терминале, где вы
их запускаете — и он совпадает с servers[0].url в
артефакте OpenAPI. Демон всё ещё привязывается
к 0.0.0.0, чтобы удалённый хост мог достичь его, но не направляйте DAEMON_URL
на этот хост в открытом виде: bearer-токен, который может управлять оболочкой,
читается любым на пути. Подключайтесь к не-loopback хосту через TLS (см. ниже).
--no-web сохраняет перечисленные ниже маршруты, но отключает ресурсы Web Shell и
зависимые поверхности: на macOS — маршруты /live/* и сокет /live/host, а на
каждой платформе — GET /mcp-app-sandbox. Передавайте токен через переменную
окружения, а не через --token, который доступен любому локальному пользователю
через /proc/<pid>/cmdline.
Примеры Bash ниже передают заголовок Authorization через файловый дескриптор
с помощью встроенной команды printf оболочки, не включая токен в аргументы curl.
Они требуют Bash, curl и jq. Для доступа с других устройств завершите TLS, как
описано в
HTTPS / TLS для мобильного и кросс-устройственного доступа;
демон затем обслуживает https:// на том же порту, поэтому переэкспортируйте
DAEMON_URL со схемой https:// перед запуском команд ниже.
Маршруты, которые реально использует интеграция
Большая часть того, что регистрирует демон, существует для работы Web Shell — операции git, установка расширений, доверие к рабочему пространству, голос, запланированные задачи — и меняется вместе с этим UI. Нижеприведённое подмножество на порядок меньше.
Это то, что нужно REST-интеграции. Остальное считайте внутренним.
Обнаружение
| Маршрут | Назначение |
|---|---|
GET /health | Liveness-проба |
GET /capabilities | Предварительный запрос — прочитайте workspaceCwd и policy.permission до всего остального |
Жизненный цикл сессии
| Маршрут | Назначение |
|---|---|
POST /session | Создание. Отправьте sessionScope: "thread" для независимого разговора |
DELETE /session/:id | Закрытие. Сохранённая сессия остаётся и может быть перезагружена |
POST /session/:id/load · POST /session/:id/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 | Модель верхнего уровня, режим и состояние параметров конфигурации; виртуальные субагенты возвращают пустой state |
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 |
Минимальный поток
1. Предварительный запрос. Прочитайте workspaceCwd (чтобы не передавать cwd при создании) и
policy.permission (чтобы знать, кто может отвечать на запросы разрешений).
curl -sH @<(printf 'Authorization: Bearer %s\n' "$QWEN_SERVER_TOKEN") "$DAEMON_URL/capabilities"2. Создание сессии. Используйте sessionScope: "thread", если вызывающие не должны
разделять один разговор — значение по умолчанию "single" заставляет повторное создание
в том же рабочем пространстве переиспользовать существующую сессию, сериализуя
независимых вызывающих через одну очередь.
SESSION_JSON="$(curl -sX POST "$DAEMON_URL/session" \
-H @<(printf 'Authorization: Bearer %s\n' "$QWEN_SERVER_TOKEN") -H 'Content-Type: application/json' \
-d '{"sessionScope":"thread"}')" || echo "create failed (curl exit $?)" >&2
printf '%s\n' "$SESSION_JSON"
SID="$(printf '%s' "$SESSION_JSON" | jq -er '.sessionId // empty')"
export SID
: "${SID:?no sessionId in the create response}"
# → {"sessionId":"…","workspaceCwd":"/srv/project","attached":false}3. Подписка до отправки промпта. Запустите это во втором терминале с тем же
QWEN_SERVER_TOKEN и DAEMON_URL, и с SID, установленным в sessionId, который
напечатал шаг 2: экспорты не пересекают терминалы, поэтому переэкспортируйте токен и
DAEMON_URL там и установите SID в этот sessionId самостоятельно. Не вставляйте
блок обратно в терминал 1 — его присваивание SID= перезапишет значение,
которое используют шаги 4-6. Last-Event-ID: 0 воспроизводит с самого старого
сохранённого события — так вы ловите события, возникшие между созданием и подпиской,
в частности model_switch_failed. При подключении (значение по умолчанию
sessionScope: "single" переиспользует существующую сессию) это событие — единственный
сигнал того, что некорректный modelServiceId был отклонён, потому что ошибка
намеренно не передаётся как HTTP-ошибка. При новом создании с modelServiceId —
чего тело шага 2 не содержит — тело 200 также содержит modelApplied, false если
переключение было отклонено, и это детерминированный сигнал для действия, в отличие от
события на кольце с ограниченным буфером. Создание без modelServiceId вообще не имеет
ключа modelApplied.
# терминал 2 — переэкспортируйте то, что нужно; переменные оболочки не пересекают терминалы
# export QWEN_SERVER_TOKEN='<the token from step 1>'
# export DAEMON_URL=http://127.0.0.1:4170
SID='<sessionId from step 2>'
curl -N "$DAEMON_URL/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 "$DAEMON_URL/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
явно и решите заранее, как он отвечает: автоодобрение уже может действовать без того,
чтобы кто-либо его выбирал.
Отвечайте на маршруте сессии: он достигает владеющего рабочего пространства, когда
ровно одна активная среда выполнения владеет сессией, и никогда не возвращается
к первичному бриджу. Недоверенный не-основной владелец возвращает 403 untrusted_workspace;
основная среда выполнения освобождена от этой проверки доверия, поэтому недоверенный
основной владелец может принять голос. Неразрешённый владелец завершается с fail closed
вместо голосования на неправильной среде выполнения — 404 session_not_found,
500 ambiguous_session_owner или 503 workspace_runtime_unavailable с
Retry-After: 1 (повторить; голос не был записан). Скопируйте data.requestId из
события permission_request и установите его перед голосованием:
export REQUEST_ID='<data.requestId>'
curl -sX POST "$DAEMON_URL/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 |
| Ошибки | Классификация ошибок |
| Наблюдаемость | Наблюдаемость |
| Полный список флагов | Конфигурация |