Skip to content

Commit 377c209

Browse files
doudouOUCqwencoder
andcommitted
docs: Refresh daemon developer docs
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
1 parent 78f0635 commit 377c209

24 files changed

Lines changed: 4788 additions & 1 deletion

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-client-adapters/tui.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# TUI Daemon Adapter Draft
22

3-
## Goal
3+
> **⚠️ 已过时**:本文档描述的是早期 `DaemonTuiAdapter` spike。该 legacy adapter 仍在 `packages/cli/src/ui/daemon/` 中,但新的可复用方向是 SDK 里的共享 UI Transcript 层。当前架构请参考 [`../daemon/14-cli-tui-adapter.md`](../daemon/14-cli-tui-adapter.md)
4+
5+
---
6+
7+
## Goal (historical)
48

59
Add a flag-gated TUI transport that talks to `qwen serve` through
610
`DaemonSessionClient` instead of creating an in-process `Config` + agent

docs/developers/daemon/00-index.md

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

0 commit comments

Comments
 (0)