Skip to content

Commit 88c66de

Browse files
committed
docs(developers): add daemon-mode developer deep-dive documentation set (Chinese)
Adds 21-file Chinese developer documentation set under docs/developers/daemon/ for qwen serve daemon mode. Plus one nav entry in docs/developers/_meta.ts and a sub-nav under docs/developers/daemon/_meta.ts. The set complements (not replaces) the existing daemon docs: - docs/users/qwen-serve.md — operator quickstart, flags, threat model - docs/developers/qwen-serve-protocol.md — wire-level HTTP routes - docs/developers/examples/daemon-client-quickstart.md — TS walkthrough - docs/developers/daemon-client-adapters/* — adapter design drafts - docs/design/f2-mcp-transport-pool.md — F2 design v2.2 It is the developer architecture reference that's been missing — a new contributor's path through the system, with one architectural diagram doc + per-component deep-dives + a quickstart/operations chapter. Document set: Foundation - 00-index.md navigation, glossary, reading paths - 01-architecture.md system architecture + 6 Mermaid diagrams Server core - 02-serve-runtime.md runQwenServe bootstrap, Express, lifecycle, validatePolicyConfig + InvalidPolicyConfigError - 03-acp-bridge.md @qwen-code/acp-bridge package internals - 04-permission-mediation.md MultiClientPermissionMediator — four policies, N=floor(M/2)+1 quorum + M=2 unanimity edge case, X-Qwen-Client-Id self-declaration security caveat - 05-mcp-transport-pool.md McpTransportPool (F2) including pool-aware snapshot fields (entryCount / entrySummary), MCPCallInterruptedError on silent transport drop, canonicalOAuth fingerprint normalization, extension-uninstall orphan reap via MAX_IDLE_MS, IDE-close drain path, /mcp refresh pool gate - 06-mcp-budget-guardrails.md WorkspaceMcpBudget — modes, hysteresis, refused-batch coalescing, scope: 'pool' future reservation - 07-workspace-filesystem.md WorkspaceFileSystem sandbox + FsError preservation over ACP wire - 08-session-lifecycle.md create/attach/load/resume, identity, heartbeat, eviction - 09-event-schema.md typed event schema v1 (29 known event types) with payloads, reducers, envelope-level metadata (_meta.serverTimestamp), tool-call _meta (provenance + serverId), SDK reducer behavior (awaitingResync flag, RESYNC_PASSTHROUGH_TYPES) - 10-event-bus.md EventBus + Last-Event-ID replay + ring-eviction-on-resume state_resync_required recovery flow - 11-capabilities-versioning.md capability registry, protocol version, conditional advertisement - 12-auth-security.md bearer, host allowlist, mutation gate, --require-auth, /health exemption, device-flow, X-Qwen-Client-Id identity caveat, sanitizeForStderr CVE-2021-42574 defense Clients - 13-sdk-daemon-client.md TypeScript SDK — DaemonClient, DaemonSessionClient, DaemonAuthFlow, SSE, MCP_RESTART_DEFAULT_TIMEOUT_MS 330s rationale - 14-cli-tui-adapter.md DaemonTuiAdapter - 15-channel-adapters.md DaemonChannelBridge + DingTalk / WeChat / Telegram - 16-vscode-ide-adapter.md DaemonIdeConnection (loopback-only) Reference appendices - 17-configuration.md env vars + CLI flags + settings.json keys - 18-error-taxonomy.md typed errors per layer (filesystem boundary, bridge errors, daemon-host error kinds, boot-time config errors, device-flow) - 19-observability.md QWEN_SERVE_DEBUG, debug recipes, /demo console Quickstart / operations - 20-quickstart-operations.md 9 startup recipes; full CLI flag / env / settings.json reference; boot fail-loud scenarios; curl validation checklist; /demo console usage; full call chain from `qwen serve` to listening server with line numbers; embedded-mode examples; graceful vs force shutdown Each topic doc follows: 概览 / 职责 / 架构 / 流程 (with Mermaid diagrams) / 状态与生命周期 / 依赖 / 配置 / 注意 & 已知局限 / 参考. Pinned to the daemon_mode_b_main code surface (F2 pool, F3 mediator, F4 prereqs). Some referenced files (McpTransportPool, state_resync_ required, validatePolicyConfig, FsError-over-wire, sanitizeForStderr) land on main only when daemon_mode_b_main next batches up — the doc set arrives ahead of the feature merges by design. History: prior revision of this PR was bilingual EN+ZH; switched to Chinese-only per maintainer preference and added 20-quickstart- operations.md to consolidate startup / validation / call-chain content that surfaced during review.
1 parent 48b0a8b commit 88c66de

23 files changed

Lines changed: 4513 additions & 0 deletions

docs/developers/_meta.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export default {
2121
'channel-plugins': 'Channel Plugin Guide',
2222
tools: 'Tools',
2323
'qwen-serve-protocol': 'qwen serve HTTP protocol',
24+
daemon: 'Daemon 模式 · 开发者深度指南',
2425

2526
examples: {
2627
display: 'hidden',

docs/developers/daemon/00-index.md

Lines changed: 149 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
# Daemon 架构
2+
## 概览
3+
4+
一个 `qwen serve` 进程坚持 **一 daemon = 一 workspace** 的不变式。它内嵌一个 Express HTTP 服务、持有一个 `acp-bridge` 实例、拉起一个 ACP 子进程(`qwen --acp`)来跑真正的 agent 运行时。多个客户端(CLI TUI、IDE companion、IM channel 机器人、Web BFF、自定义脚本)通过 HTTP + SSE 连进来,要么共享同一个 ACP session(`sessionScope: 'single'`,默认),要么每个客户端各拿一个(`per-client`)。
5+
6+
在 ACP 子进程内部,MCP server 通过 `McpTransportPool`(F2)实现工作区内共享:一对 (server name + 配置指纹) 对应一条 MCP transport,不管被几个 session 发现都只起一份。Bridge 的 `MultiClientPermissionMediator`(F3)在四种策略之一下协调多客户端的权限投票。
7+
8+
本文给出 **系统级全景**,本文档集其余 18 篇文档都挂在它下面。每条主干流程都给一张 Mermaid 时序图,单个组件的实现细节请看对应的专题文档。
9+
10+
## 进程拓扑
11+
12+
```mermaid
13+
flowchart LR
14+
subgraph clients["Clients"]
15+
WUI["Web UI<br/>(packages/webui/src/daemon)"]
16+
TUI["CLI TUI<br/>(待迁移到 SDK ui/*)"]
17+
IDE["VSCode IDE<br/>(packages/vscode-ide-companion)"]
18+
CH["Channel bots<br/>(DingTalk / WeChat / Telegram)"]
19+
SDK["Any SDK consumer<br/>(packages/sdk-typescript/src/daemon)"]
20+
end
21+
22+
subgraph daemon["qwen serve process (one workspace)"]
23+
EXP["Express app<br/>(packages/cli/src/serve/server.ts)"]
24+
BR["AcpBridge<br/>(packages/acp-bridge/src/bridge.ts)"]
25+
MED["MultiClientPermissionMediator<br/>(F3)"]
26+
EB["EventBus per session<br/>(eventBus.ts)"]
27+
FS["WorkspaceFileSystem<br/>(cli/src/serve/fs/)"]
28+
end
29+
30+
subgraph child["ACP child process (qwen --acp)"]
31+
AGT["QwenAgent runtime"]
32+
POOL["McpTransportPool<br/>(F2, core/src/tools)"]
33+
BDG["WorkspaceMcpBudget"]
34+
end
35+
36+
subgraph external["External"]
37+
MCP1["MCP server A<br/>(stdio)"]
38+
MCP2["MCP server B<br/>(websocket)"]
39+
end
40+
41+
WUI -- "HTTP+SSE" --> EXP
42+
TUI -- "HTTP+SSE" --> EXP
43+
IDE -- "HTTP+SSE (loopback)" --> EXP
44+
CH -- "HTTP+SSE" --> EXP
45+
SDK -- "HTTP+SSE" --> EXP
46+
47+
EXP --> BR
48+
BR --> MED
49+
BR --> EB
50+
EXP --> FS
51+
52+
BR -- "ACP NDJSON over stdio" --> AGT
53+
AGT --> POOL
54+
POOL --> BDG
55+
POOL -- "shared transport" --> MCP1
56+
POOL -- "shared transport" --> MCP2
57+
```
58+
59+
要点:
60+
61+
- daemon 进程与 ACP 子进程通过 `AcpChannel` 连接,默认是真实的子进程 + 一对管道;`inMemoryChannel` 用于测试。
62+
- 所有架构都被这条「daemon ↔ child」缝隙塑造:HTTP / SSE 在 daemon 终止,agent 决策与工具调用在子进程发生,bridge 是中转。
63+
64+
## 包关系
65+
66+
```mermaid
67+
flowchart TB
68+
subgraph serve["packages/cli/src/serve"]
69+
RQS["runQwenServe.ts<br/>(bootstrap)"]
70+
SRV["server.ts (Express)"]
71+
CAP["capabilities.ts"]
72+
AUTH["auth.ts"]
73+
FSM["fs/ (sandbox)"]
74+
DSP["daemonStatusProvider.ts"]
75+
end
76+
77+
subgraph br["packages/acp-bridge"]
78+
BR2["bridge.ts"]
79+
BC2["bridgeClient.ts"]
80+
EB2["eventBus.ts"]
81+
MED2["permissionMediator.ts"]
82+
ST2["status.ts"]
83+
CH2["channel.ts / spawnChannel.ts"]
84+
end
85+
86+
subgraph core["packages/core/src/tools"]
87+
POOL2["mcp-transport-pool.ts"]
88+
ENT["mcp-pool-entry.ts"]
89+
WBG["mcp-workspace-budget.ts"]
90+
SMV["session-mcp-view.ts"]
91+
end
92+
93+
subgraph sdk["packages/sdk-typescript/src/daemon"]
94+
DC["DaemonClient.ts"]
95+
DSC["DaemonSessionClient.ts"]
96+
EVT["events.ts"]
97+
SSE["sse.ts"]
98+
AUTHF["DaemonAuthFlow.ts"]
99+
UI["ui/* (#4328 + #4353)<br/>normalizer / transcript / store / render"]
100+
end
101+
102+
subgraph adapters["Adapters"]
103+
WUIP["webui/src/daemon/<br/>DaemonSessionProvider.tsx"]
104+
CHB["channels/base/<br/>DaemonChannelBridge.ts"]
105+
DT["channels/dingtalk"]
106+
WX["channels/weixin"]
107+
TG["channels/telegram"]
108+
IDEA["vscode-ide-companion/<br/>daemonIdeConnection.ts"]
109+
end
110+
111+
RQS --> SRV
112+
RQS --> CAP
113+
RQS --> AUTH
114+
RQS --> FSM
115+
RQS --> BR2
116+
117+
BR2 --> BC2
118+
BR2 --> EB2
119+
BR2 --> MED2
120+
BR2 --> CH2
121+
122+
BR2 -.spawns.-> core
123+
POOL2 --> ENT
124+
POOL2 --> WBG
125+
POOL2 --> SMV
126+
127+
WUIP --> DSC
128+
WUIP --> UI
129+
CHB --> DSC
130+
DT --> CHB
131+
WX --> CHB
132+
TG --> CHB
133+
IDEA --> DSC
134+
135+
DSC --> DC
136+
DC --> EVT
137+
DC --> SSE
138+
DC --> AUTHF
139+
UI --> EVT
140+
```
141+
142+
记住三条信任边界:
143+
144+
1. HTTP 入口边界:`serve/auth.ts` 中间件链。
145+
2. bridge ↔ ACP 子进程边界:stdio 上的 NDJSON,没有认证 —— 子进程默认信任 bridge。
146+
3. agent ↔ MCP server 边界:agent 可能触发涉及宿主资源的工具调用。
147+
148+
## 流程 1:HTTP 请求生命周期
149+
150+
```mermaid
151+
sequenceDiagram
152+
autonumber
153+
participant C as Client (SDK)
154+
participant MW as Middleware<br/>(bearer→host→CORS→mutationGate)
155+
participant R as Route handler
156+
participant BR as AcpBridge
157+
participant BC as BridgeClient
158+
participant CH as ACP child
159+
160+
C->>MW: POST /session/:id/prompt<br/>Authorization: Bearer …<br/>X-Qwen-Client-Id: …
161+
MW->>MW: bearerAuth (constant-time compare)
162+
MW->>MW: hostAllowlist (DNS rebinding guard)
163+
MW->>MW: denyBrowserOriginCors
164+
MW->>MW: mutationGate (strict on mutating routes)
165+
MW->>R: req validated
166+
R->>BR: bridge.sendPrompt(sessionId, body, clientId)
167+
BR->>BC: client.sendPrompt(sessionId, …)
168+
BC->>CH: ACP JSON-RPC over stdin
169+
CH-->>BC: ACP response / notifications
170+
BC-->>BR: result
171+
BR-->>R: result
172+
R-->>C: 200 JSON
173+
```
174+
175+
非流式路由(prompt、cancel、model 切换、metadata、workspace CRUD)以一次 JSON 响应结束。流式输出**不是**在该 HTTP 连接上以分块方式返回,而是走 SSE 通道;见流程 2。
176+
177+
## 流程 2:SSE 事件投递与重放
178+
179+
```mermaid
180+
sequenceDiagram
181+
autonumber
182+
participant C as Client
183+
participant SR as GET /session/:id/events
184+
participant EB as EventBus<br/>(per session)
185+
participant BC as BridgeClient
186+
participant CH as ACP child
187+
188+
C->>SR: GET …/events<br/>Last-Event-ID: 42 (optional)
189+
SR->>EB: subscribe(lastSeenId=42, maxQueued=N)
190+
EB-->>SR: replay frames 43..currentTail<br/>(from ring buffer)
191+
SR-->>C: NDJSON: id=43, type=session_update, …
192+
CH-->>BC: ACP notification (e.g. agent_message_chunk)
193+
BC->>EB: publish({type, data})
194+
EB-->>SR: enqueue id=N
195+
SR-->>C: id=N, type=…, data=…
196+
Note over EB,SR: If subscriber queue >= maxQueued,<br/>EventBus emits client_evicted terminal frame<br/>and closes subscriber.
197+
```
198+
199+
要点:
200+
201+
- 环形缓冲有上限(`eventRingSize`,默认 1024)。
202+
- 重连客户端如果 `Last-Event-ID` 已经落出环外,会收到合成 catch-up 信号,必须用 `loadSession` / `resumeSession` 重建更深层状态。
203+
- 慢消费者在队列 75% 触发 `slow_client_warning`,达到上限时收到 `client_evicted`(终态)后被关掉。
204+
205+
## 流程 3:多客户端权限协调
206+
207+
```mermaid
208+
sequenceDiagram
209+
autonumber
210+
participant CH as ACP child (agent)
211+
participant BC as BridgeClient.requestPermission
212+
participant MED as Mediator (policy)
213+
participant EB as EventBus
214+
participant C1 as Client A<br/>(originator)
215+
participant C2 as Client B
216+
217+
CH->>BC: ACP requestPermission(requestId, options)
218+
BC->>MED: request({requestId, sessionId, originatorClientId, allowedOptionIds}, timeoutMs)
219+
MED->>EB: publish permission_request<br/>(broadcast to subscribers)
220+
EB-->>C1: SSE permission_request
221+
EB-->>C2: SSE permission_request
222+
223+
alt first-responder
224+
C2->>MED: POST /permission/:requestId optionId=allow
225+
MED-->>BC: resolved
226+
BC-->>CH: ACP response
227+
MED->>EB: permission_resolved
228+
C1->>MED: POST /permission/:requestId (late vote)
229+
MED-->>C1: 409 permission_already_resolved
230+
else designated
231+
C2->>MED: vote (clientId != originatorClientId)
232+
MED-->>C2: 403 permission_forbidden
233+
C1->>MED: vote (matches originator)
234+
MED-->>BC: resolved
235+
else consensus (N-of-M)
236+
C1->>MED: vote
237+
MED->>EB: permission_partial_vote (1/N)
238+
C2->>MED: vote
239+
MED->>EB: permission_partial_vote (2/N)
240+
Note over MED: when tally reaches quorum on one option, resolve
241+
else local-only
242+
C2->>MED: vote (remote)
243+
MED-->>C2: 403 permission_forbidden (remote_not_allowed)
244+
Note over MED,CH: blocks until a loopback voter resolves it
245+
end
246+
```
247+
248+
跨策略「逃生口」:任何客户端都可以投 `CANCEL_VOTE_SENTINEL` 把请求短路成 `cancelled / agent_cancelled`。bridge 防止 wire 端通过普通 `optionId` 字段偷偷塞这个哨兵(`InvalidPermissionOptionError`)。
249+
250+
四种策略一句话:
251+
252+
- `first-responder` — 第一个有效投票获胜(默认,保留 live 协作 UX)。
253+
- `designated` — 只有 `originatorClientId` 能投,其他客户端收 `permission_forbidden`
254+
- `consensus` — N-of-M 法定人数,过程中发 `permission_partial_vote` 让 UI 渲染进度。
255+
- `local-only` — 拒绝任何 HTTP 投票,只接受 loopback。
256+
257+
## 流程 4:MCP transport 池的 acquire / release / restart
258+
259+
```mermaid
260+
sequenceDiagram
261+
autonumber
262+
participant S as Session in ACP child
263+
participant P as McpTransportPool
264+
participant SIF as spawnInFlight (dedup)
265+
participant E as PoolEntry
266+
participant BDG as WorkspaceMcpBudget
267+
participant SRV as MCP server
268+
269+
S->>P: acquire(name, cfg, sessionId)
270+
P->>SIF: check inflight for (name+fingerprint)
271+
alt cached inflight
272+
SIF-->>P: existing promise
273+
else cold start
274+
P->>BDG: tryReserve(name)
275+
BDG-->>P: ok / refused
276+
alt refused
277+
P-->>S: BudgetExhaustedError
278+
else ok
279+
P->>E: new PoolEntry(...)
280+
E->>SRV: connect transport
281+
SRV-->>E: ready
282+
E-->>P: connected
283+
end
284+
end
285+
P->>P: sessionToEntries.add(sessionId, id)
286+
P-->>S: PooledConnection
287+
288+
Note over S,P: Session uses entry, then…
289+
290+
S->>P: release(id, sessionId)
291+
P->>E: detach session
292+
E->>E: arm drain timer (default 30s)
293+
Note over E: refs==0 → drain timer fires → close transport<br/>(MAX_IDLE_MS 5min hard cap survives flap)
294+
295+
Note over S,P: Operator restart flow…
296+
S->>P: restartByName(name, opts?)
297+
P->>E: drain + close
298+
P->>E: spawn replacement
299+
E->>SRV: reconnect
300+
P->>EB: publish mcp_server_restarted
301+
```
302+
303+
要点:
304+
305+
- `releaseSession(sessionId)` 借助 `sessionToEntries` 反向索引,以 O(refs) 释放该 session 持有的所有条目。
306+
- daemon 关停时 `drainAll()``draining` 标志(拒绝新的 acquire),并以可配置超时等待所有条目关闭。
307+
- `restartByName` 可以接 `entryIndex` 来精确重启某条;池里同名多条目时返回 `{entries: RestartResult[]}` 形状。
308+
309+
## 流程 5:生命周期 —— 启动与优雅退出
310+
311+
```mermaid
312+
sequenceDiagram
313+
autonumber
314+
participant Op as Operator (signal)
315+
participant RQS as runQwenServe
316+
participant APP as Express app
317+
participant BR as AcpBridge
318+
participant CH as ACP child
319+
320+
Op->>RQS: qwen serve --workspace … --token …
321+
RQS->>RQS: validate flags + canonicalize workspace
322+
RQS->>RQS: allocate PermissionAuditRing
323+
RQS->>BR: createHttpAcpBridge(options)
324+
RQS->>APP: createServeApp(bridge, …)
325+
RQS->>APP: listen(host, port)
326+
RQS->>RQS: arm SIGINT / SIGTERM handlers
327+
328+
Op->>RQS: SIGTERM
329+
RQS->>BR: dispose device-flow registry
330+
RQS->>BR: bridge.shutdown()
331+
BR->>CH: send graceful close (10s deadline)
332+
CH-->>BR: exit
333+
RQS->>APP: server.close() (5s force-close timer)
334+
APP->>APP: closeAllConnections() (+2s secondary)
335+
Note over Op,RQS: Second SIGTERM during shutdown →<br/>bridge.killAllSync() + process.exit(1) (orphan prevention)
336+
```
337+
338+
为什么要分两阶段:
339+
340+
- 还在飞的 HTTP 请求、还连着的 SSE 订阅者、子进程里还在跑的工具调用都需要有上限的退出窗口。
341+
- 任何一条卡过窗口,force-close 路径会接管,避免子进程把 daemon 进程拖住。
342+
- 第二次 SIGTERM 直接走 `bridge.killAllSync()` + `process.exit(1)`,防孤儿。
343+
344+
## 关键文件
345+
346+
| 关注点 | 文件 |
347+
| ------------------ | -------------------------------------------------------------------- |
348+
| Bootstrap | `packages/cli/src/serve/runQwenServe.ts` (308-994) |
349+
| Express 应用 | `packages/cli/src/serve/server.ts` (261-339) |
350+
| 能力注册表 | `packages/cli/src/serve/capabilities.ts` (37-215) |
351+
| Auth 中间件 | `packages/cli/src/serve/auth.ts` (1-60) |
352+
| Bridge | `packages/acp-bridge/src/bridge.ts` |
353+
| BridgeClient | `packages/acp-bridge/src/bridgeClient.ts` |
354+
| 权限协调器 | `packages/acp-bridge/src/permissionMediator.ts` (1-1292) |
355+
| EventBus | `packages/acp-bridge/src/eventBus.ts` |
356+
| MCP transport 池 | `packages/core/src/tools/mcp-transport-pool.ts` (104+) |
357+
| Workspace MCP 预算 | `packages/core/src/tools/mcp-workspace-budget.ts` |
358+
| Workspace 文件系统 | `packages/cli/src/serve/fs/` |
359+
| SDK DaemonClient | `packages/sdk-typescript/src/daemon/DaemonClient.ts` (209-1506) |
360+
| SDK SessionClient | `packages/sdk-typescript/src/daemon/DaemonSessionClient.ts` (61-385) |
361+
| 事件 schema | `packages/sdk-typescript/src/daemon/events.ts` (13-63) |
362+
363+
## 参考
364+
365+
- 设计 issue:[#3803](https://github.com/QwenLM/qwen-code/issues/3803)(daemon 总体设计)、[#4175](https://github.com/QwenLM/qwen-code/issues/4175)(F 系列里程碑)。
366+
- 用户使用文档:[`../../users/qwen-serve.md`](../../users/qwen-serve.md)
367+
- Wire 协议参考:[`../qwen-serve-protocol.md`](../qwen-serve-protocol.md)
368+
- F2 设计文档(v2.2,含 32 条 review fold-in):[`../../design/f2-mcp-transport-pool.md`](../../design/f2-mcp-transport-pool.md)
369+
- F2 设计笔记:issue [#4175](https://github.com/QwenLM/qwen-code/issues/4175) commit 4-6。

0 commit comments

Comments
 (0)