qwen serve 快速上手与运维全指南:从启动、验证到内部调用链
本篇技术指南以 Qwen Code 的 daemon 模式(qwen serve)为主题,完整覆盖如何启动本地 HTTP 服务、如何用 curl 验证其工作状态、以及从 qwen serve 命令行到监听端口的内部调用链。读完本文,你将掌握十余种面向不同场景的启动配方、全部启动参数与环境变量的语义、boot 拒绝(refusal)的显式失败场景、以及优雅/强制关闭与嵌入边界等运维要点。
1. 最短路径:一条命令启动 daemon
在任意工作目录下执行:
qwen serve
预期输出(端口、workspace 以实际环境为准):
qwen serve listening on http://127.0.0.1:4170 (mode=http-bridge, workspace=/your/cwd)
qwen serve: bound to workspace "/your/cwd"
qwen serve: trusted loopback mode; local callers have full API access without bearer authentication, including code execution as the daemon user. Use --require-auth with QWEN_SERVER_TOKEN on shared or untrusted hosts.
在浏览器中打开 http://127.0.0.1:4170/ 即可获得 Web Shell UI(聊天、会话列表与 workspace 检查)。默认的无 token 回环(loopback)主监听器赋予本地调用者完整的操作员 API 权限,同时保留 workspace 信任、会话归属、client-id、权限、feature、validation 与 resource 等全部检查。
createServeApp() 将打包好的 Web Shell 静态资源(packages/cli/src/serve/web-shell-static.ts)挂载在 bearerAuth 之前(见 server.ts 中的注释与实现),因此 shell 本身无需 token 即可加载;当配置了鉴权时,shell 自身的 API 调用会携带 bearer——启动时加 --open(token 放在 URL fragment 中,永远不会发送到服务端),或启用鉴权后手动追加 #token=…。--no-web 则完全退出 Web 界面,只保留 API。
2. 启动配方:13 种典型场景
# 1. 本地开发默认(loopback,无 token)
qwen serve
# 2. 显式指定 workspace + 临时端口
qwen serve --workspace /path/to/repo --port 0
# 3. 加固的 loopback 开发(即便在 loopback 上也强制 bearer)
QWEN_SERVER_TOKEN=$(openssl rand -hex 32) qwen serve --require-auth
# 4. 暴露到局域网(建议使用稳定 token;不提供 token 时
# daemon 会生成一次性 bearer 并打印一次)
QWEN_SERVER_TOKEN=$(openssl rand -hex 32) \
qwen serve --hostname 0.0.0.0 --port 4170
# 4b. 暴露到局域网,使用自动生成的一次性 bearer
qwen serve --hostname 0.0.0.0 --port 4170
# 5. 面向大量会话与更大的 replay ring 调优
qwen serve --max-sessions 0 --event-ring-size 32000
# 6. 多客户端协作 + 严格的 MCP 预算
QWEN_SERVER_TOKEN=secret \
qwen serve --require-auth \
--mcp-client-budget 10 \
--mcp-budget-mode enforce
# 7. 使用 settings.json 中配置的 consensus 策略启动
# settings.json: { "policy": { "permissionStrategy": "consensus", "consensusQuorum": 2 } }
qwen serve
# 8. 调试日志
QWEN_SERVE_DEBUG=1 qwen serve
# 9. 禁用 F2 池(回退到按会话的 MCP 客户端)
QWEN_SERVE_NO_MCP_POOL=1 qwen serve
# 10. 允许浏览器 Web UI 跨域访问
QWEN_SERVER_TOKEN=secret \
qwen serve --allow-origin 'http://localhost:3000'
# 11. Prompt 截止时间 + SSE 空闲超时
qwen serve --prompt-deadline-ms 300000 --writer-idle-timeout-ms 600000
# 12. 空闲 ACP 子进程在工作排空后至少保留 60s 可复用
qwen serve --channel-idle-timeout-ms 60000
# 13. 开启 HTTP 限流
QWEN_SERVE_RATE_LIMIT=1 qwen serve
关于配方 3 的特别说明:在加固的 loopback 模式下,/health 注册在 bearerAuth 之后,因此健康检查探针必须像其他 API 路由一样携带 token(Web Shell 静态面保持 pre-auth 是设计使然;如需纯 API daemon 请加 --no-web)。
3. 全部启动参数(Full Startup Flags)
CLI 定义于 packages/cli/src/commands/serve.ts(yargs builder 中逐项声明,handler 中完成 boot 前置校验)。以下为完整参数表:
| Flag | 类型 | 默认值 | 何时必填 | 作用 |
|---|---|---|---|---|
--port <n> |
number | 4170 |
- | TCP 端口;0 表示由操作系统分配临时端口。 |
--hostname <host> |
string | 127.0.0.1 |
非 loopback 绑定在空 token 源、或 localhost 解析到非 loopback 且无 token 源时拒绝启动 |
绑定地址。loopback 值包括任意 127.0.0.0/8 地址、localhost、::1、[::1];localhost 只解析一次并固定,trusted 模式会在启动完成前校验实际监听地址。[::1] 的方括号会自动剥离;host:port 输入会被拒绝并提示改用 --port。非 loopback 结果必定携带 bearer:若 --token 与 QWEN_SERVER_TOKEN 都未提供,boot 会生成一个临时 bearer 而非拒绝。例外:生成与否取决于拼写——解析到非 loopback 的 localhost 永不生成,只有在解析出 token 源时才启动;而解析到 loopback 的非字面主机名会生成并打印其 bearer token。 |
--token <s> |
string | env / 无 | 非 loopback 拼写无需有效源(会自动生成,但空值会拒绝);在 loopback 上:--require-auth、--allow-origin '*' 或非 loopback HTTP(S) 的 --allow-origin 时需要 |
Bearer token;启动时裁剪一次。显式空值是“已提供”的源,因此会遮蔽 QWEN_SERVER_TOKEN 并解析为无 token——非 loopback 绑定随即拒绝启动,解析到非 loopback 且无其他 token 源的 localhost 绑定同样拒绝(对它永不生成)。它会出现在 /proc/<pid>/cmdline 中,因此优先使用 QWEN_SERVER_TOKEN。boot 的 stderr 也会对此发出警告(源码见 serve.ts)。 |
--max-sessions <n> |
number | 32 |
- | 每 workspace 的活跃会话上限。超出后新的 spawn 请求返回 503。0 表示无限制。NaN/负数会抛出异常。 |
--max-total-sessions <n> |
number | 800,或容量 ≤ 25 时推导 |
- | 全 daemon 的活跃会话上限。省略时:注册容量 > 25(含默认 256)时为 800(即使只有一个 workspace);容量 ≤ 25 时从每 workspace 上限与启动/恢复的 workspace 数量推导一次,其中一个 workspace 无限制。动态注册不会重算。0 表示无限制。 |
--memory-budget-mb <n> |
integer,[1024, 1048576] |
cgroup/主机内存的 50% | - | daemon 进程树的总内存预算,以解析出的可用内存为上限。不对任何子进程做尺寸分配;当前唯一消费者是自适应 live-journal 增长池(见 --max-journal-bytes)。在 limits.memory 中报告,含建模的每子进程分区。 |
--max-journal-events <n> |
正 safe integer | 10000 |
- | 每会话 in-flight liveJournal replay 条目的基线上限。自适应增长可提升该值(见 --max-journal-bytes);固定任一 journal 参数会禁用增长。 |
--max-journal-bytes <n> |
正 safe integer | 8388608 |
- | 每会话 in-flight liveJournal 的基线字节上限。超限的 turn 会按需增长 cap(向翻倍方向,受剩余池头寸限制),池为有效 --memory-budget-mb 的 5%(上限 1024 MB;当有效预算低于 1024 MB 下限时为 0,即禁用增长),绝不越过每会话 256 MiB 硬上限;固定任一 journal 参数会禁用增长。 |
--memory-pressure-mode <mode> |
off | observe |
observe |
仅观测模式 | 两种模式下都报告 runtime.memory.pressure;只有 observe 会触发 daemon_memory_pressure issue。仅限根进程。 |
--child-heap-mode <mode> |
off | observe |
observe |
仅观测模式 | observe 下在 limits.memory.childHeap 报告建模分区;不应用、不拒绝任何东西。off 下该块的 two figures 为 null。 |
--max-pending-prompts-per-session <n> |
number | 5 |
- | 每会话已接受但待处理/运行中的 prompt 上限。超出返回 503。0/Infinity 表示无限制。负数或非整数抛异常。 |
--workspace <dir> |
string / 可重复 | process.cwd() |
- | 启动 workspace 运行时;重复使用可注册额外的隔离运行时,第一个为 primary。每个值必须是绝对路径、必须存在、必须是目录。boot 通过 canonicalizeWorkspace 规范化每个值。cwd 不匹配的 POST /session 返回 400 workspace_mismatch。 |
--max-connections <n> |
number | 256 |
- | 监听器级别 server.maxConnections。0/Infinity 无限制。NaN/负数导致 boot 失败,避免 fail-open。 |
--require-auth |
boolean | false |
loopback 上需要 token 源 | 将 bearer 鉴权扩展到 loopback 以及 /health。无 token 源时 boot 拒绝启动——这是仅限 loopback 的 fail-fast,因为非 loopback 绑定上生成的临时 token 满足该标志。loopback 源包括 --token、QWEN_SERVER_TOKEN 与 --open-with-auth(后者在 boot 前安装自己生成的 token,因此 --require-auth --open-with-auth 可以启动)。 |
--enable-session-shell |
boolean | false |
bearer 或 trusted loopback | 启用直接 POST /session/:id/shell 执行。调用方还必须发送会话绑定的 X-Qwen-Client-Id。 |
--event-ring-size <n> |
number | 8000 |
- | 每会话 SSE replay ring 深度。软上限 MAX_EVENT_RING_SIZE = 1_000_000;越界值在 bridge 构造时抛出。默认值与 bridge 共用同一常量 DEFAULT_RING_SIZE(见 serve.ts)。 |
--http-bridge |
boolean | true |
- | Bridge 模式:生产环境尝试预热一个 primary qwen --acp 子进程,首次使用失败后重试;trusted 次级按需启动一个,untrusted 次级无法启动 ACP。Stage 2 进程内模式尚未实现;--no-http-bridge 会回退并打印到 stderr。 |
--mcp-client-budget <n> |
number | 无 | mcp-budget-mode=enforce 时必填 |
workspace MCP 客户端上限。必须是正整数。 |
--mcp-budget-mode <m> |
'enforce' | 'warn' | 'off' |
设置预算时为 warn,否则 off |
enforce 需要 --mcp-client-budget |
enforce 拒绝超额连接(按 mcpServers 声明顺序确定性判定,disabledReason: "budget");warn 在 75% 时仅警告;off 纯观测。 |
--allow-origin <pattern> |
可重复 string | 无 | - | 替换默认 Origin 拒绝策略的 CORS 白名单。通配符与非 loopback HTTP(S) origin 需要已解析的 token;这些守卫在生成之后读取 token,因此非 loopback 绑定上生成的临时 bearer 即可满足,拒绝仅限 loopback。 |
--allow-private-auth-base-url |
boolean | false |
- | 允许安装 localhost/私有网络 auth provider 的 baseUrl。仅限受信任的本地开发使用。 |
--prompt-deadline-ms <n> |
number | 无 | - | 服务端 prompt 墙钟限制(ms);超时中止 prompt。 |
--writer-idle-timeout-ms <n> |
number | 无 | - | 每 SSE 连接的空闲超时(ms)。 |
--channel-idle-timeout-ms <n> |
非负整数 | 0 |
- | 运行时工作排空后 ACP 子进程的自动回收延迟。保留普通预热供首次使用。活跃 keepalive 窗口可能延长配置的延迟,取剩余更长的延迟。 |
--initialize-timeout-ms <n> |
number | 10000 |
- | ACP 子进程启动截止时间(channel factory + initialize 握手)及默认请求超时(ms)。 |
--session-reap-interval-ms <n> |
number | 60000 |
- | 会话 reaper 扫描间隔。0 禁用。 |
--session-idle-timeout-ms <n> |
number | 1800000 |
- | 断连会话的空闲超时。0 禁用。 |
--rate-limit / --no-rate-limit |
boolean | env / off | - | 启用/禁用按层级 HTTP 限流。 |
--rate-limit-prompt <n> |
number | 10 |
--rate-limit |
窗口内 prompt 请求数。 |
--rate-limit-mutation <n> |
number | 30 |
--rate-limit |
窗口内 mutation 请求数。 |
--rate-limit-read <n> |
number | 120 |
--rate-limit |
窗口内读请求数。 |
--rate-limit-window-ms <n> |
number | 60000 |
--rate-limit |
限流窗口长度;必须 >= 1000。 |
在 handler 中,--mcp-client-budget 必须为正整数、--mcp-budget-mode=enforce 必须携带预算,否则直接 process.exit(1)(见 serve.ts);--memory-budget-mb 通过 isValidMemoryBudgetMb 校验;设置 --rate-limit 后各 tier 参数会先回退读取 QWEN_SERVE_RATE_LIMIT_* 环境变量再校验正整数(serve.ts)。
4. 环境变量
| Env | 等价 flag / 作用 |
|---|---|
QWEN_SERVER_TOKEN |
等价于 --token;--token 优先。boot 时裁剪一次,避免 cat token.txt 的尾部换行。两个源都未设置时,非 loopback 绑定会生成临时 bearer;daemon 从不自行设置该变量。已提供但为空的源绝不“缺席”,因此总是抑制生成——但空值只在单方向上决定解析出的 token:空 --token 遮蔽此处设置的值并解析为无 token(非 loopback 绑定随即拒绝),而此处的空值仅在未传 --token 时解析为无 token——非空 --token 仍然胜出,daemon 基于它启动。解析到非 loopback 的 localhost 绑定同样永不生成,只在解析出 token 源时启动(见 --hostname)。 |
QWEN_SERVE_DEBUG |
1/true/on/yes(不区分大小写)启用详细 stderr 日志。 |
QWEN_SERVE_NO_MCP_POOL |
1 完全禁用 workspace MCP 池,回退到按会话的 McpClientManager。Capabilities 停止通告 mcp_workspace_pool/mcp_pool_restart。 |
QWEN_SERVE_MCP_CLIENT_BUDGET |
ACP 子进程内部预算输入。CLI 通过 childEnvOverrides 从 --mcp-client-budget 生成;不是父进程 env 回退。 |
QWEN_SERVE_MCP_BUDGET_MODE |
ACP 子进程内部预算模式。CLI 通过 childEnvOverrides 从 --mcp-budget-mode 生成;不是父进程 env 回退。 |
QWEN_SERVE_PROMPT_DEADLINE_MS |
--prompt-deadline-ms 的 env 回退。 |
QWEN_SERVE_WRITER_IDLE_TIMEOUT_MS |
--writer-idle-timeout-ms 的 env 回退。 |
QWEN_SERVE_MCP_POOL_TRANSPORTS |
由 ACP 子进程读取。逗号分隔的池化传输白名单;默认 stdio,websocket。 |
QWEN_SERVE_MCP_POOL_DRAIN_MS |
由 ACP 子进程读取。池条目空闲排空延迟;默认 30000,限制在 1000..600000 ms。 |
QWEN_SERVE_RATE_LIMIT |
1/true 启用限流;CLI flag 优先。 |
QWEN_SERVE_RATE_LIMIT_PROMPT |
--rate-limit-prompt 的 env 回退。 |
QWEN_SERVE_RATE_LIMIT_MUTATION |
--rate-limit-mutation 的 env 回退。 |
QWEN_SERVE_RATE_LIMIT_READ |
--rate-limit-read 的 env 回退。 |
QWEN_SERVE_RATE_LIMIT_WINDOW_MS |
--rate-limit-window-ms 的 env 回退。 |
按 handle 的 env 覆盖是有意为之:同一进程内运行两个 daemon 不会在 process.env 上竞争。defaultSpawnChannelFactory 在 spawn 时快照 env。
5. settings.json 同样会被读取
Boot 会调用一次 loadSettings(boundWorkspace):
| Key | 类型 | 行为 |
|---|---|---|
policy.permissionStrategy |
'first-responder' | 'designated' | 'consensus' | 'local-only' |
设置 BridgeOptions.permissionPolicy。Boot 用 validatePolicyConfig 校验;未知值抛出 InvalidPolicyConfigError 而非静默回退。 |
policy.consensusQuorum |
正整数 | consensus 策略的 N。默认 floor(M/2)+1。在非 consensus 策略下设置会被忽略,boot 在 stderr 打印警告。 |
context.fileName |
string | 控制 POST /workspace/init 通过 workspace-service 的 contextFilename 写入哪个文件。 |
tools.disabled |
string[] | 通过 normalizeDisabledToolList() 规范化(trim、丢弃空项、去重)后影响下一个 ACP 子进程 spawn。 |
tools.approvalMode |
string | 默认会话审批模式。 |
telemetry |
object | OTel 配置:enabled、otlpEndpoint、otlpProtocol、各信号端点等。详见 17-configuration.md。 |
settings 的 I/O 失败(如 JSON 损坏)回退到默认值。InvalidPolicyConfigError 是例外:策略配置错误会显式导致 boot 失败。
6. Boot 拒绝场景(显式失败)
run-qwen-serve.ts 在以下场景有意抛出而非回退:
需要先厘清:非 loopback 绑定且完全没有 token 源不是拒绝——daemon 会生成一个临时的 128-bit bearer(16 随机字节、22 个 base64url 字符),启动时打印一次,每次重启都会轮换(详见 qwen-serve.md 的 remote quickstart)。Loopback 拼写从不生成。--open-with-auth 的生成器是独立的且更大——32 字节、256-bit bearer——且只在 loopback 上运行。
所有 token 守卫读取的都是解析后的 token,即生成步骤之后的值。这正是 --require-auth 与 --allow-origin 的拒绝仅限 loopback 的原因:非 loopback 绑定上已有生成的 bearer 就位。
| 场景 | 错误前缀 |
|---|---|
非 loopback 绑定且显式空 token 源(--token '' / QWEN_SERVER_TOKEN='') |
Refusing to bind ... without a bearer token |
loopback 绑定且无 token 源时设置 --require-auth(--token、QWEN_SERVER_TOKEN、--open-with-auth) |
Refusing to start with --require-auth set but no bearer token |
--workspace 不存在、不是目录或不是绝对路径 |
Invalid --workspace ... |
--workspace stat 权限被拒 |
Invalid --workspace ...: permission denied |
--mcp-client-budget 不是正整数 |
Must be a positive integer |
--mcp-budget-mode=enforce 无预算 |
requires a positive mcpClientBudget |
--hostname 写成了 localhost:4170 |
looks like a "host:port" combination. Use --port |
--hostname [::1]:8080 |
Invalid --hostname ... brackets indicate an IPv6 literal but the value is not a clean [addr] form |
--max-connections 为 NaN 或负数 |
Must be >= 0 |
--event-ring-size > 1_000_000 |
bridge 构造时抛出 |
无 token 的 loopback 绑定上 --allow-origin '*' |
Refusing to start with --allow-origin '*' but no bearer token configured |
无 token 的 loopback 绑定上非 loopback HTTP(S) --allow-origin |
Refusing to start with --allow-origin ... but no bearer token configured |
--prompt-deadline-ms / --writer-idle-timeout-ms 不是正整数 |
Must be a positive integer |
--initialize-timeout-ms 不是正整数或超过 2^31-1 |
Must be a positive integer / Exceeds maximum JS timer delay |
未知 policy.permissionStrategy 或非正 policy.consensusQuorum |
InvalidPolicyConfigError |
7. curl 验证清单
# 1. 存活检查
curl http://127.0.0.1:4170/health
# -> {"status":"ok"}
# 1.1 深度健康检查
curl -s 'http://127.0.0.1:4170/health?deep=1' | jq
# 2. 能力列表
curl -s http://127.0.0.1:4170/capabilities | jq
# 3. 预检就绪状态
curl -s http://127.0.0.1:4170/workspace/preflight | jq
# 4. 环境快照(密钥只报告是否存在)
curl -s http://127.0.0.1:4170/workspace/env | jq
# 5. MCP 池 / 预算快照
curl -s http://127.0.0.1:4170/workspace/mcp | jq
# 6. 创建会话
curl -s -X POST http://127.0.0.1:4170/session \
-H 'Content-Type: application/json' \
-H 'X-Qwen-Client-Id: curl-debug' \
-d '{}' | jq
# 7. 订阅 SSE(将 <sid> 替换为真实会话 id)
curl -N \
-H 'Accept: text/event-stream' \
-H 'X-Qwen-Client-Id: curl-debug' \
-H 'Last-Event-ID: 0' \
'http://127.0.0.1:4170/session/<sid>/events'
# 8. Web Shell UI
open http://127.0.0.1:4170/
启用 bearer 鉴权时,每个请求都要加上 -H "Authorization: Bearer $QWEN_SERVER_TOKEN"。在生成 token 的流程中该变量永远不会被设置——daemon 在启动时打印一次 bearer 且不导出它——因此需要先把打印的值粘贴到自己的 shell 中:
TOKEN='<printed bearer>'
curl -s -H "Authorization: Bearer $TOKEN" http://<bind>:4170/capabilities | jq
8. 有浏览器 UI 吗?—— Web Shell 详解
有——就是 Web Shell。 resolveWebShellDir() 定位构建产物(发布时打包在 CLI bundle 旁,checkout 时位于 packages/web-shell/dist),mountWebShellAssets() 在 /、/assets 和 /session/:id 文档导航上提供它们(浏览器深链——直接 curl /session/<id> 拿到的是 API 的 401/404,而不是 shell)。当产物缺失时 daemon 降级为仅 API 而不是崩溃;--no-web 显式退出。
静态 shell 在所有启动模式下都挂载在 bearerAuth 之前——浏览器无法在地址栏导航或 <script src> 子资源上附加 Authorization 头,对它们上锁只会破坏 UI。API 调用在无 token 的 primary 监听器上使用 trusted-loopback 权威,或在鉴权模式下附加配置的 bearer。在带 token 的非 loopback 绑定上,shell 的同源 HTTP 请求无需白名单:Origin 等于直接 socket 协议加规范化 Host 权威的请求会被 bearer 鉴权,然后在 CORS 墙前剥离,因此它的 POST 可以通行。仍有三种情况需要显式 --allow-origin <origin>:terminal 与 voice 功能背后的 WebSocket 升级;浏览器通过 TLS 终结代理访问 daemon 时(其 https origin 永远匹配不上明文 socket);以及任何重写 Host 头的纯 HTTP 中间件(nginx 默认的 proxy_set_header Host $proxy_host、k8s Ingress),其转发的 Host 不再匹配浏览器的 Origin。转发头从不被信任用于建立同源,因此解决方式是白名单条目或原样转发 Host 的代理;仅端口翻译(docker -p 8080:4170)在非 loopback 绑定上无需任何配置,因为只比较转发的 Host——在默认的 loopback 绑定上,Host 门禁会对翻译后的端口返回 403 Invalid Host header(包括 shell 文档在内的每个请求),且 --allow-origin 无法覆盖它,所以要么转发同一端口(ssh -L 4170:localhost:4170),要么绑定非 loopback。
CSP 由 buildWebShellCsp() 构建,刻意比静态页面更宽松(内联 performance.measure 补丁需要 'unsafe-inline'、shiki 与 mermaid 需要 eval/wasm/blob workers、katex 字体需要 data:、SSE 需要 connect-src 'self')。frame-ancestors 'none' 加 X-Frame-Options: DENY 阻止点击劫持,除非通过 --allow-origin 显式允许某个扩展 origin,以便 UI 可托管在 Chrome 侧边栏(issue #5626)。
如需原始协议检查,可直接订阅 SSE 流(routes/sse-events.ts)——参见第 7 节的 curl 配方。
9. 从 qwen serve 到监听端口的内部调用链
qwen serve
|
v (process)
packages/cli/index.ts main()
|
v
llm.tsx main() - parseArguments()
|
v (yargs assembly)
config/config.ts import { serveCommand } ...
config/config.ts .command(serveCommand)
config/config.ts await yargsInstance.parse()
|
v (handler)
commands/serve.ts handler(argv) - boot pre-checks
commands/serve.ts const { runQwenServe } = await import('../serve/index.js') # lazy load
commands/serve.ts await runQwenServe({...})
|
v
serve/run-qwen-serve.ts runQwenServe(opts, deps)
| |- resolve token (trim / env fallback / non-loopback ephemeral generation)
| |- hostname mismatch fallback
| |- auth preflight (reads the resolved token)
| |- workspace validation + canonicalization
| |- MCP budget validation + childEnvOverrides
| |- loadSettings + validatePolicyConfig
| |- PermissionAuditRing + publisher
| |- resolveBridgeFsFactory
| `- createHttpAcpBridge({...})
|
v
serve/run-qwen-serve.ts const app = createServeApp(opts, () => actualPort, {...})
|
v
serve/server.ts createServeApp() - builds Express app (**does not listen**)
| |- middleware chain (loopback Origin strip / access log + trace id / Host allowlist / remote same-origin strip / CORS / pre-auth health + shell / webhooks / bearerAuth / rate limit / JSON / telemetry / per-route mutation gate)
| |- route mounting (health / web-shell static / capabilities / workspace / session / SSE / ACP HTTP)
| `- return app
|
v
serve/run-qwen-serve.ts server = createServer(app) / https.createServer(..., app)
| |- lifecycle.bindServer(server, { startupReady, drainHost })
| |- server.listen(port, hostname)
| |- server.maxConnections = cap
| |- actualPort = server.address().port
| |- write "qwen serve listening on ..."
| |- register SIGINT / SIGTERM (onSignal)
| `- resolve(handle: RunHandle)
|
v
commands/serve.ts await blockForever() // block forever until signal
关键事实:
createServeApp只构建,不监听。 它返回一个挂载了中间件与路由的express()实例。仅普通嵌入者可以继续自己app.listen()。使用 Live/Conversations 的嵌入者必须在监听前把真实 Node server 绑定到导出的 app lifecycle,并在关闭时 await 该 lifecycle。() => actualPort是惰性闭包。actualPort在server.listen回调中赋值。hostAllowlist中间件按需读取它,因此临时端口(--port 0)仍能正确门禁Host头。await blockForever()是有意为之。 如果yargs.parse()解析完成,CLI 顶层会落入交互式 TUI 入口(llm.tsx)。SIGINT/SIGTERM 通过runQwenServe的onSignal路径退出。该函数实现于 serve.ts,是一个永不 resolve 的 Promise。
10. HTTP 路由文件拆分
主装配发生在 server.ts 的 createServeApp() 中,它串联中间件并挂载聚焦的路由模块:
| 路由 | 文件 | 挂载入口 |
|---|---|---|
/health |
packages/cli/src/serve/routes/health.ts | healthRoutes.register() |
/daemon/status |
packages/cli/src/serve/routes/daemon-status.ts | registerDaemonStatusRoutes() |
/capabilities、workspace init/tool/MCP mutation 路由、ACP HTTP bridge |
packages/cli/src/serve/server.ts | 在 createServeApp() 内直接注册 |
| Workspace status、env、preflight、MCP/tool/provider/skill 摘要 | packages/cli/src/serve/routes/workspace-status.ts | registerWorkspaceStatusRoutes()、registerWorkspaceDiagnosticStatusRoutes() |
| Workspace 扩展与扩展操作 | packages/cli/src/serve/routes/workspace-extensions.ts | registerWorkspaceExtensionRoutes() |
/workspace/memory(GET/POST) |
packages/cli/src/serve/workspace-memory.ts | mountWorkspaceMemoryRoutes() |
所有 /workspace/agents CRUD 路由 |
packages/cli/src/serve/workspace-agents.ts | mountWorkspaceAgentsRoutes() |
GET /file、/file/bytes、/list、/glob、/stat |
packages/cli/src/serve/routes/workspace-file-read.ts | registerWorkspaceFileReadRoutes() |
POST /file/write、/file/edit |
packages/cli/src/serve/routes/workspace-file-write.ts | registerWorkspaceFileWriteRoutes() |
| Workspace setup、trust、settings、permissions、voice 路由 | packages/cli/src/serve/routes/workspace-*.ts | registerWorkspaceSetupGithubRoutes()、registerWorkspaceTrustRoutes() 等 |
| Workspace auth provider 与 device-flow 路由 | packages/cli/src/serve/routes/workspace-auth.ts | registerWorkspaceAuthRoutes() |
| Session lifecycle、prompt、metadata、language、shell、recap、rewind、branch、list 路由 | packages/cli/src/serve/routes/session.ts | registerSessionRoutes() |
GET /session/:id/events SSE 流 |
packages/cli/src/serve/routes/sse-events.ts | registerSseEventsRoutes() |
| Permission 响应路由 | packages/cli/src/serve/routes/permission.ts | registerPermissionRoutes() |
完整的路由与线协议参考见 qwen-serve-protocol.md;架构详见 01-architecture.md。
11. 优雅关闭 vs 强制关闭
- 第一次 SIGINT / SIGTERM ->
runQwenServe的onSignal-> 两阶段优雅关闭:bridge.shutdown():每个 channel 先给KILL_HARD_DEADLINE_MS(10 秒),然后channel.kill()。server.close():in-flight 请求排空,SHUTDOWN_FORCE_CLOSE_MS(5 秒,见 run-qwen-serve.ts)触发closeAllConnections(),随后再应用 2 秒的第二截止时间。
- 关闭过程中再次 SIGINT / SIGTERM ->
bridge.killAllSync()同步 SIGKILL 所有 ACP 子进程并调用process.exit(1),避免产生孤儿进程。
runQwenServe 返回的 RunHandle.close() 是仓库内部宿主与测试使用的编程等价物。
12. 嵌入边界(Embedding Boundary)
runQwenServe、createServeApp 及其 lifecycle 辅助函数是内部实现 API;发布的 @qwen-code/qwen-code 包不导出 ./serve 子路径。外部集成应启动 qwen serve --no-web 并使用文档化的 HTTP/SSE 协议或 @qwen-code/sdk。仓库代码与测试可以直接导入源码模块,但这些导入不是受支持的集成契约。
对于仓库内部调用 createServeApp 的场景,默认 fsFactory.trusted = false。Agent 侧的 ACP writeTextFile 会被以 untrusted_workspace 拒绝,并打印一次 stderr 警告。要么注入带显式信任的 deps.fsFactory,要么注入 deps.bridge,要么接受 trust-gated 的默认行为。
13. 调试配方
常见命令(详见 19-observability.md 的调试章节):
# daemon 是否存活?
curl http://127.0.0.1:4170/health
# 通告了哪些能力?
curl -s http://127.0.0.1:4170/capabilities | jq
# Daemon 宿主就绪状态
curl -s http://127.0.0.1:4170/workspace/preflight | jq
# 订阅实时 SSE
curl -N -H 'Accept: text/event-stream' \
-H 'Last-Event-ID: 0' \
'http://127.0.0.1:4170/session/<sid>/events'
# 详细日志
QWEN_SERVE_DEBUG=1 qwen serve
参考
- CLI 入口:packages/cli/src/commands/serve.ts
- Bootstrap:packages/cli/src/serve/run-qwen-serve.ts
- Express 工厂:packages/cli/src/serve/server.ts
- 中间件:packages/cli/src/serve/auth.ts
- Bridge 工厂:packages/acp-bridge/src/bridge.ts
- Web Shell 静态挂载:packages/cli/src/serve/web-shell-static.ts
- 用户文档:docs/users/qwen-serve.md
- 线协议:docs/developers/qwen-serve-protocol.md
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.25 K640- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python760
SlideSCIPPT插件,支持素材库、AI助手、一键添加图片标题,复制粘贴位置、一键图片对齐、一键插入Markdown(加粗、超链接等行内样式、代码块、LaTeX等块级样式)、便捷导出图片!C#531
Agent-Reach给你的 AI Agent 一键装上互联网能力。13 个平台(网页/GitHub/YouTube/小红书/B站/Twitter/Reddit 等)多后端路由,当下最稳的接入方式替你选好、装好、体检好。GitHub 主仓库同步镜像。Python1214
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go22945
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java37151