关联:PR #4472(ACP Streamable HTTP transport /acp)、#3803(daemon open decisions)、#4175(Mode B roadmap)。
这是一条架构提案(design-first),先评审分层与 L2 收口方案,通过后再起实现 PR。
背景与问题
/acp(PR #4472)已对齐所有 bridge-backed 能力;要做成 REST+SSE 的完全等价替代,还差 4 类能力——文件 I/O、设备流登录、agents CRUD、memory CRUD。它们当前不在 HttpAcpBridge 上,REST 路由直连 route 级服务(WorkspaceFileSystemFactory / DeviceFlowRegistry / SubagentManager / writeWorkspaceContextFile),绕过了共享层。
同时审计发现 HttpAcpBridge 已名不副实:它横跨"应用能力层(L2)"与"对子进程的 ACP-client 出站腿(L3)",还兼任一批 daemon 本地工作区操作——已偏 God-object。
提案(推荐:完整 C)
抽出与 bridge 平级的 DaemonWorkspaceService(L2 兄弟,不经 bridge 委托),把 HttpAcpBridge 瘦身/改名为 AcpSessionBridge。拆分原则 = 是否依赖 ACP child:依赖 live child → 留会话桥;纯 daemon 本地 → 进 WorkspaceService。两组件互不依赖,trust/TOCTOU/audit/每请求 context 封装为单点,REST 与 /acp 两个传输都调它。
不可动摇的原则:业务逻辑 + trust/audit 不能写在 L1 route handler 里;两个传输调用同一个传输无关的 L2。(这是通用软件架构问题,不是 ACP 要求——ACP 是 wire 协议,对内部分层无约束。)
方案对比(A 扩 bridge / B 直调服务 / C 新建门面)、决定性判据、client 接入样例、落点映射等完整内容见下方文档。
待评审决策点
- L2 收口选 A / B / C?(提案推荐 C;理由见文档 §6)
- 是否同意
HttpAcpBridge → AcpSessionBridge 改名 + 把纯本地工作区操作迁入 DaemonWorkspaceService?
- 迁移分期:前置 PR(建 service + REST 改走 service)→ 后续
/acp PR(加 _qwen/fs|auth|workspace/agent|memory)。
验收(完成后)
- REST 与
/acp 对 file/auth/agents/memory 走同一 L2,trust/audit 单点;/acp 达到与 REST 完全等价。
AcpSessionBridge 仅保留会话/child 相关职责;DaemonWorkspaceService 内聚 daemon 本地能力。
附:完整架构文档
acpBridge 的架构角色、分层模型与 client 接入规范
范围:qwen serve daemon 的分层架构,HttpAcpBridge 的真实职责,以及"其他 client 应该接入哪一层"。
基准:worktree qwen-code-acp-http(含 ACP-over-HTTP PR #4472)。日期:2026-05-26。
性质:架构评估快照;file:line 为撰写时状态,引用前请核对当前代码。
关联:同目录 daemon-architecture-overview.md(全景与多维评估);PR #4472;issue #3803 / #4175。
0. TL;DR
HttpAcpBridge 已不只是"ACP 桥"——它横跨 应用/能力层(L2) 与 对子进程的 ACP-client 出站腿(L3) 两层职责,事实上是 daemon 的服务门面,名字已名不副实。
- L2 当前是裂开的:一半能力在 bridge(会话/事件/权限/部分工作区操作),一半散在 route 级服务(文件 I/O、设备流、agents、memory)。这是要收口的核心问题。
- 接入规则(端口-适配器):所有 client 接 L1 传输适配器(
/acp 标准 或 REST 既有);L1 必须薄,业务/鉴权/审计下沉 L2;任何 client 都不得直连 L3/L4(自起子进程、绕过 token/工作区信任边界)。
- 已知漂移:
channels/AcpBridge.ts 的 standalone 模式自起子进程,直达 L3/L4,绕过 daemon —— 应统一走 L1→L2,或明确成独立部署形态。
1. HttpAcpBridge 的真实职责(代码佐证)
当前 HttpAcpBridge(packages/acp-bridge/src/bridge.ts + bridgeTypes.ts)的 ~31 个方法分两类:
1.1 真正的 ACP-child 操作(会调 connection.*,发往子进程)
spawnOrAttach / loadSession / resumeSession(→ connection.newSession / loadSession)
sendPrompt(→ connection.prompt)、cancelSession(→ connection.cancel)
- 权限请求经
requestPermission 回流(agent→client)
1.2 daemon 本地操作(完全不碰 child,只读设置/文件/状态)
getWorkspaceMcpStatus / Skills / Providers / Env / Preflight
initWorkspace、setWorkspaceToolEnabled、restartMcpServer
listWorkspaceSessions、recordHeartbeat、getHeartbeatState
updateSessionMetadata、getSessionContextStatus、getSessionSupportedCommandsStatus
1.3 还内含「对子进程的 ACP-client 适配器」
ChannelInfo.connection = new ClientSideConnection(..., channel.stream),channel.stream = ndJsonStream(child stdin/stdout)。
- 还有 EventBus(每会话 ring + Last-Event-ID 重放 + 背压/驱逐)、权限 mediator、单子进程多路复用 N 会话的生命周期。
结论:bridge = 应用门面(L2) + ACP-client 出站腿(L3) 二合一。它已经承担了一批"非 ACP"的 daemon 工作区操作,所以并不是纯粹的协议桥。
1.4 不在 bridge 上的能力(缺口)
file I/O(/file /glob /list /stat /file/write /file/edit)、设备流登录(/workspace/auth/*)、agents CRUD(/workspace/agents)、memory CRUD(/workspace/memory)—— 这些只在 REST 路由里,直连 route 级服务(WorkspaceFileSystemFactory / DeviceFlowRegistry / SubagentManager / writeWorkspaceContextFile),没有进 L2 门面。
2. 分层架构图
CLIENTS(actors)
webui TS/Java/Py SDK 钉钉/微信/TG channels Zed/Goose(ACP原生) future
│ │ │ │ │
════╪══════════════╪══════════════════╪═══════════════════════╪════════════════╪═══ L1 传输适配器 (inbound adapters)
REST+SSE REST+SSE DaemonChannelBridge /acp [WS*]
server.ts sdk/daemon →(走 REST) acpHttp/ (deferred)
└──────────────┴──────────────────┴───────────────────────┘
│ ★ 所有适配器都应汇入同一层,且自身保持"薄" ★
════════════════════════╪══════════════════════════════════════════════════ L2 应用/能力层(应当唯一)
┌───────────────┴────────────────────────────┐ ┌─────────────────────────────┐
│ HttpAcpBridge(façade,现状) │ │ route 级服务(现状裂在 L2 之外)│
│ • 会话/通道生命周期(1 child 多路复用) │ │ WorkspaceFileSystemFactory │
│ • EventBus(每会话 ring/重放/背压) │ │ DeviceFlowRegistry │
│ • 权限仲裁 mediator │ │ SubagentManager │
│ • daemon 本地工作区操作(status/init/tool/ │ │ writeWorkspaceContextFile │
│ mcp-restart) │ │ ← file/auth/agents/memory │
└───────────────┬──────────────────────────────┘ │ 缺口:未并入 façade │
│ └─────────────────────────────┘
════════════════════════╪══════════════════════════════════════════════════ L3 ACP 出站腿(→ agent)
│ ClientSideConnection / ndJsonStream (stdio JSON-RPC)
▼
════════════════════════════════════════════════════════════════════════════ L4 Agent(子进程)
qwen --acp : sessions Map → GeminiChat(模型回合) + 每会话 MCP pool → core engine
┄┄ 漂移(drift) ┄┄ channels/AcpBridge.ts 自己 spawn child ─────────────────────► 直达 L3/L4,绕过 L1+L2 daemon
层定义:
- L1 传输适配器:把某种 wire 协议编解码成内部调用。REST+SSE(
server.ts)、/acp(acpHttp/,JSON-RPC over HTTP + 双 SSE)、未来 WebSocket。必须薄——只做协议编解码 + 路由 + 鉴权拦截,不放业务。
- L2 应用/能力层:传输无关的能力面 + 单一的 trust/TOCTOU/audit 执行点。目标是唯一;现状裂成 bridge façade + 旁挂服务。
- L3 ACP 出站腿:daemon 作为 ACP client 对子进程 agent 的适配器(stdio ndJsonStream)。当前与 L2 融合在 bridge 里。
- L4 Agent:
qwen --acp 子进程,跑 core 引擎 + 每会话 MCP pool;ACP agent 角色。
注意方向:northbound(L1,client→daemon)daemon 是 ACP agent;southbound(L3,daemon→child)daemon 是 ACP client。bridge 同时是 northbound 能力门面和 southbound client 适配器。
3. 其他 client 应该接入哪一层?(接入规范)
规则(端口-适配器 / hexagonal):
| 谁 |
接哪层 |
说明 |
| 所有外部 client(编辑器 / SDK / web / channel) |
L1(选一个传输适配器) |
ACP 原生客户端 → /acp(标准);既有 web/SDK → REST+SSE。不要为新 client 再造第三套 bespoke 传输 |
| L1 适配器自身 |
保持薄:协议编解码 + 路由 + 鉴权拦截 |
业务 / 鉴权决策 / 审计一律下沉 L2 |
| 新增能力(file/auth/agents/memory…) |
加在 L2,由所有 L1 适配器共享暴露 |
绝不写在某个 L1 handler 里(否则 REST 与 /acp 必然漂移、trust/audit 两套实现) |
| 任何 client |
不得直连 L3/L4 |
不能自己起 child、不能绕过 daemon 的 bearer token + 单工作区信任边界 |
3.1 两个直接结论
- channels standalone(
channels/AcpBridge.ts 自起 child)= 架构漂移:一个 client 直达 L3/L4,绕过 L1+L2 的鉴权/单工作区/审计。应统一走 DaemonChannelBridge → L1(REST 或 /acp) → L2;若保留 standalone,应明确成"另一种部署形态",不作默认。
- 未来 client 一律接 L1 的
/acp(标准 ACP,零 qwen 胶水),而非新开路由——这正是引入 /acp 的核心价值。
4. L2 收口(统一能力层)—— 待决方案 A/B/C
把 file/auth/agents/memory 从"L1 路由直连服务"收进统一 L2,有三个选项(细节见 #3803 / #4175 评论与 daemon-acp-http 设计文档 §17.3):
| 方案 |
做法 |
取舍 |
| A |
扩到 HttpAcpBridge |
与已在 bridge 上的同类工作区操作一致;/acp 只依赖 bridge。代价:继续撑大已偏 façade 的 bridge |
| B |
保留独立服务,REST 与 /acp 都直接调 |
最轻、bridge 不膨胀;代价:/acp 需注入这些服务 |
| C |
新建 DaemonWorkspaceService 门面,两传输共用 |
内聚/命名最干净;代价:多一层 |
不可动摇的原则(与具体选项无关):业务逻辑 + trust/TOCTOU/audit 不能写在 L1 route handler 里,REST 与 /acp 两个传输都要调用同一个传输无关的 L2。
这是通用软件架构问题(端口-适配器 / 单一事实源 / DRY),不是 ACP 的要求——ACP 是 wire 协议,对 daemon 内部分层无任何约束。
倾向 A(一致性最好、/acp 只依赖 bridge),前提是承认 HttpAcpBridge 现实上已是 daemon 服务门面(可考虑改名);若要保住其 ACP 纯粹性则选 B/C。最终待 owner 拍板。
5. 可选的进一步演进(非本轮必须)
- 拆 L2 与 L3:把 bridge 内的
ClientSideConnection(对 child 的 ACP-client 适配器)从能力门面里分出,让 L2 纯做能力、L3 纯做"对 agent 的传输"。便于 Stage 2 in-process(去子进程)时只换 L3。
- L1 收敛:长期把 REST 做成
/acp 的薄 compat shim(设计文档既定方向),最终 L1 只剩 /acp(+WS) 一套标准面 + 一层 REST 兼容垫片。
- 重命名
HttpAcpBridge → 更贴合其 daemon-façade 现实的名字(若采纳 A)。
6. 方案评估与推荐(L2 收口:A vs B vs C)
把 file/auth/agents/memory 从"L1 路由直连服务"收进统一 L2,三个选项:
- A:扩到
HttpAcpBridge。
- B:保留独立服务,REST 与 /acp 都直接调。
- C:新建
DaemonWorkspaceService 门面(L2 兄弟),两传输共用。
6.1 决定性判据:trust/audit/context 的封装点
这 4 类能力真正的风险不是功能,而是 trust gate / TOCTOU / audit / 每请求上下文(clientId、loopback、audit originator) 的执行(REST 现在用 fsFactory.forRequest({ originatorClientId, route }) 这样穿线)。核心问题:这套上下文穿线封装在一处(两传输都穿不错),还是各穿一遍(会漂移)?
| 判据 |
A 扩 bridge |
B 两传输直调服务 |
C 新建 WorkspaceService |
| trust/context 封装为单点 |
✅ |
❌ 两处穿线→漂移 |
✅ |
| 内聚 / 单一职责 |
❌ 撑大 God-object |
⚠️ "一袋松散服务"不算层 |
✅ |
| 与现状一致性 |
✅ bridge 已有同类操作 |
❌ 加剧分裂 |
⚠️ 最干净(需迁现有操作才彻底) |
| /acp 耦合 |
✅ 只依赖 bridge |
➖ 多 4 依赖 |
➖ 多 1 依赖 |
| 改动量/风险 |
低 |
低(但复制穿线) |
中 |
| 利于 Stage 2 / L2-L3 拆分 / 改名 |
❌ 全焊死 bridge |
➖ 中性 |
✅ 天然分离 |
排序:架构质量 C > A > B;在唯一要紧的轴(trust 单点封装)上 A、C 合格,B 不合格。
6.2 推荐:完整 C(按"更清晰、分层更合理、耦合更少"取舍)
抽出与 bridge 平级的 DaemonWorkspaceService(不经 bridge 委托——委托反而加耦合),HttpAcpBridge 瘦身/改名为 AcpSessionBridge:
L1 REST+SSE /acp [WS*]
│ │ │ │
┌─────────────┘ └─────┐ ┌───┘ └────┐
▼ ▼ ▼ ▼
══ L2(两个聚焦兄弟,互不依赖)═══════════════════════════════
┌──────────────────────────┐ ┌──────────────────────────────┐
│ AcpSessionBridge │ │ DaemonWorkspaceService(新) │
│ • 会话/通道生命周期 │ │ • file I/O │
│ • prompt/cancel/EventBus │ │ • device-flow auth │
│ • 权限仲裁 │ │ • agents CRUD │
│ • 依赖 child 的内省/状态 │ │ • memory CRUD │
│ (mcp/skills/preflight…) │ │ • workspace init/tool/env │
└───────────┬──────────────┘ │ • 统一 RequestContext │
│ L3 → child └──────────────────────────────┘
拆分原则 = 是否依赖 ACP child: 依赖 live child(prompt/会话/mcp·skills·preflight 状态/context)→ 留 AcpSessionBridge;纯 daemon 本地(文件/登录/agents/memory/init/tool/env)→ 进 DaemonWorkspaceService。两组件互不依赖;跨切需求(如 tool-toggle 走 EventBus)用单向注入(service 持 event-publish 回调,不反向)。
为什么满足三条:更清晰(各一职责、命名诚实);分层合理(L1 薄 → L2 聚焦 → L3 连接,trust/context 单点封装);耦合更少(bridge 不背工作区杂活、service 不依赖 bridge、两传输按域调聚焦接口)。
代价(已知):比 A 改动大——新建 service + 统一 context;REST 现有 file/auth/agents/memory 路由改为调 service;把 bridge 上纯本地工作区操作迁入 service;/acp 新增 _qwen/fs|auth|workspace/agent|memory 方法。鉴于优先架构质量,此代价值得。
重申:这是通用软件架构问题(端口-适配器/单一事实源/DRY),不是 ACP 要求——ACP 是 wire 协议,对内部分层无约束。
7. Client 接入样例
核心:L2 拆分对 client 完全透明。 client 只连一个 L1 传输面、一套 token;daemon 内部把"会话域"路由给 AcpSessionBridge、"工作区域"路由给 DaemonWorkspaceService。
7.1 ACP 原生 client 走 /acp(一条连接,跨两个域)
BASE=http://127.0.0.1:8767/acp ; TOK="Authorization: Bearer devtoken"
# 1) 握手 → 响应头 Acp-Connection-Id: C1 + capabilities(_meta.qwen.methods)
curl -sD- -XPOST $BASE -H "$TOK" -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize"}'
# 2) 连接级 SSE(承载 session/new 回复 + 工作区调用回复)
curl -sN $BASE -H "$TOK" -H 'acp-connection-id: C1' &
# ── 会话域(→ AcpSessionBridge)──
# 3) 建会话(202;回复落 conn SSE: {id:2,result:{sessionId:S1}})
curl -XPOST $BASE -H "$TOK" -H 'acp-connection-id: C1' -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"session/new","params":{}}'
# 4) 会话级 SSE(session/update、request_permission)
curl -sN $BASE -H "$TOK" -H 'acp-connection-id: C1' -H 'acp-session-id: S1' &
# 5) prompt(202;update 流 + 最终 result 落 session SSE)
curl -XPOST $BASE ... -d '{"jsonrpc":"2.0","id":3,"method":"session/prompt","params":{"sessionId":"S1","prompt":[{"type":"text","text":"hi"}]}}'
# 6) 切模型(标准方法)
curl -XPOST $BASE ... -d '{"jsonrpc":"2.0","id":4,"method":"session/set_config_option","params":{"sessionId":"S1","configId":"model","value":"qwen-max"}}'
# ── 工作区域(→ DaemonWorkspaceService)—— 同一连接,无需 session ──
curl -XPOST $BASE ... -d '{"jsonrpc":"2.0","id":5,"method":"_qwen/workspace/agents"}' # [Follow-up]
curl -XPOST $BASE ... -d '{"jsonrpc":"2.0","id":6,"method":"_qwen/fs/read","params":{"path":"src/app.ts"}}' # [Follow-up]
curl -XPOST $BASE ... -d '{"jsonrpc":"2.0","id":7,"method":"_qwen/workspace/mcp"}' # 已实现
第 5–7 步无 sessionId、与会话调用共用同一连接与 conn SSE——client 感知不到它们落到不同 L2 组件。
7.2 Web/SDK client 走 REST+SSE(等价)
会话域: POST /session → {sessionId:S1}; GET /session/S1/events (SSE);
POST /session/S1/prompt; POST /session/S1/model
工作区域: GET /workspace/agents; GET /file?path=…; GET /workspace/mcp
7.3 落点映射(client 调用 → L1 → L2)
| client 调用 |
L1 |
L2 组件 |
session/new·prompt·cancel·set_config_option、SSE session/update |
/acp 或 REST |
AcpSessionBridge |
_qwen/workspace/agents·_qwen/fs/read·_qwen/workspace/memory·_qwen/auth/* |
/acp 或 REST |
DaemonWorkspaceService |
_qwen/workspace/{mcp,skills,providers,env,preflight}、session/context |
/acp 或 REST |
依赖 child→Bridge;纯本地→Service |
7.4 SDK
DaemonClient/DaemonSessionClient(sdk-typescript/src/daemon/)把 REST+SSE 封装成方法;未来给 /acp 出对等 helper(工作区域用 SDK extMethod('_qwen/…'))。无论哪种,client 看到统一 API,L2 拆分始终隐藏在 L1 之后。
标 [Follow-up] 的 _qwen/fs/*·_qwen/workspace/agents·_qwen/auth/*·_qwen/workspace/memory 为 C 方案落地后才有;其余现已实现。
背景与问题
/acp(PR #4472)已对齐所有 bridge-backed 能力;要做成 REST+SSE 的完全等价替代,还差 4 类能力——文件 I/O、设备流登录、agents CRUD、memory CRUD。它们当前不在HttpAcpBridge上,REST 路由直连 route 级服务(WorkspaceFileSystemFactory/DeviceFlowRegistry/SubagentManager/writeWorkspaceContextFile),绕过了共享层。同时审计发现
HttpAcpBridge已名不副实:它横跨"应用能力层(L2)"与"对子进程的 ACP-client 出站腿(L3)",还兼任一批 daemon 本地工作区操作——已偏 God-object。提案(推荐:完整 C)
抽出与 bridge 平级的
DaemonWorkspaceService(L2 兄弟,不经 bridge 委托),把HttpAcpBridge瘦身/改名为AcpSessionBridge。拆分原则 = 是否依赖 ACP child:依赖 live child → 留会话桥;纯 daemon 本地 → 进 WorkspaceService。两组件互不依赖,trust/TOCTOU/audit/每请求 context 封装为单点,REST 与/acp两个传输都调它。不可动摇的原则:业务逻辑 + trust/audit 不能写在 L1 route handler 里;两个传输调用同一个传输无关的 L2。(这是通用软件架构问题,不是 ACP 要求——ACP 是 wire 协议,对内部分层无约束。)
方案对比(A 扩 bridge / B 直调服务 / C 新建门面)、决定性判据、client 接入样例、落点映射等完整内容见下方文档。
待评审决策点
HttpAcpBridge→AcpSessionBridge改名 + 把纯本地工作区操作迁入DaemonWorkspaceService?/acpPR(加_qwen/fs|auth|workspace/agent|memory)。验收(完成后)
/acp对 file/auth/agents/memory 走同一 L2,trust/audit 单点;/acp达到与 REST 完全等价。AcpSessionBridge仅保留会话/child 相关职责;DaemonWorkspaceService内聚 daemon 本地能力。附:完整架构文档
acpBridge 的架构角色、分层模型与 client 接入规范
0. TL;DR
HttpAcpBridge已不只是"ACP 桥"——它横跨 应用/能力层(L2) 与 对子进程的 ACP-client 出站腿(L3) 两层职责,事实上是 daemon 的服务门面,名字已名不副实。/acp标准 或 REST 既有);L1 必须薄,业务/鉴权/审计下沉 L2;任何 client 都不得直连 L3/L4(自起子进程、绕过 token/工作区信任边界)。channels/AcpBridge.ts的 standalone 模式自起子进程,直达 L3/L4,绕过 daemon —— 应统一走 L1→L2,或明确成独立部署形态。1.
HttpAcpBridge的真实职责(代码佐证)当前
HttpAcpBridge(packages/acp-bridge/src/bridge.ts+bridgeTypes.ts)的 ~31 个方法分两类:1.1 真正的 ACP-child 操作(会调
connection.*,发往子进程)spawnOrAttach/loadSession/resumeSession(→connection.newSession/loadSession)sendPrompt(→connection.prompt)、cancelSession(→connection.cancel)requestPermission回流(agent→client)1.2 daemon 本地操作(完全不碰 child,只读设置/文件/状态)
getWorkspaceMcpStatus/Skills/Providers/Env/PreflightinitWorkspace、setWorkspaceToolEnabled、restartMcpServerlistWorkspaceSessions、recordHeartbeat、getHeartbeatStateupdateSessionMetadata、getSessionContextStatus、getSessionSupportedCommandsStatus1.3 还内含「对子进程的 ACP-client 适配器」
ChannelInfo.connection = new ClientSideConnection(..., channel.stream),channel.stream = ndJsonStream(child stdin/stdout)。结论:bridge = 应用门面(L2) + ACP-client 出站腿(L3) 二合一。它已经承担了一批"非 ACP"的 daemon 工作区操作,所以并不是纯粹的协议桥。
1.4 不在 bridge 上的能力(缺口)
file I/O(
/file /glob /list /stat /file/write /file/edit)、设备流登录(/workspace/auth/*)、agents CRUD(/workspace/agents)、memory CRUD(/workspace/memory)—— 这些只在 REST 路由里,直连 route 级服务(WorkspaceFileSystemFactory/DeviceFlowRegistry/SubagentManager/writeWorkspaceContextFile),没有进 L2 门面。2. 分层架构图
层定义:
server.ts)、/acp(acpHttp/,JSON-RPC over HTTP + 双 SSE)、未来 WebSocket。必须薄——只做协议编解码 + 路由 + 鉴权拦截,不放业务。qwen --acp子进程,跑 core 引擎 + 每会话 MCP pool;ACP agent 角色。3. 其他 client 应该接入哪一层?(接入规范)
规则(端口-适配器 / hexagonal):
/acp(标准);既有 web/SDK → REST+SSE。不要为新 client 再造第三套 bespoke 传输3.1 两个直接结论
channels/AcpBridge.ts自起 child)= 架构漂移:一个 client 直达 L3/L4,绕过 L1+L2 的鉴权/单工作区/审计。应统一走DaemonChannelBridge → L1(REST 或 /acp) → L2;若保留 standalone,应明确成"另一种部署形态",不作默认。/acp(标准 ACP,零 qwen 胶水),而非新开路由——这正是引入/acp的核心价值。4. L2 收口(统一能力层)—— 待决方案 A/B/C
把 file/auth/agents/memory 从"L1 路由直连服务"收进统一 L2,有三个选项(细节见 #3803 / #4175 评论与
daemon-acp-http设计文档 §17.3):HttpAcpBridgeDaemonWorkspaceService门面,两传输共用不可动摇的原则(与具体选项无关):业务逻辑 + trust/TOCTOU/audit 不能写在 L1 route handler 里,REST 与
/acp两个传输都要调用同一个传输无关的 L2。倾向 A(一致性最好、/acp 只依赖 bridge),前提是承认
HttpAcpBridge现实上已是 daemon 服务门面(可考虑改名);若要保住其 ACP 纯粹性则选 B/C。最终待 owner 拍板。5. 可选的进一步演进(非本轮必须)
ClientSideConnection(对 child 的 ACP-client 适配器)从能力门面里分出,让 L2 纯做能力、L3 纯做"对 agent 的传输"。便于 Stage 2 in-process(去子进程)时只换 L3。/acp的薄 compat shim(设计文档既定方向),最终 L1 只剩/acp(+WS) 一套标准面 + 一层 REST 兼容垫片。HttpAcpBridge→ 更贴合其 daemon-façade 现实的名字(若采纳 A)。6. 方案评估与推荐(L2 收口:A vs B vs C)
把 file/auth/agents/memory 从"L1 路由直连服务"收进统一 L2,三个选项:
HttpAcpBridge。DaemonWorkspaceService门面(L2 兄弟),两传输共用。6.1 决定性判据:trust/audit/context 的封装点
这 4 类能力真正的风险不是功能,而是 trust gate / TOCTOU / audit / 每请求上下文(clientId、loopback、audit originator) 的执行(REST 现在用
fsFactory.forRequest({ originatorClientId, route })这样穿线)。核心问题:这套上下文穿线封装在一处(两传输都穿不错),还是各穿一遍(会漂移)?排序:架构质量 C > A > B;在唯一要紧的轴(trust 单点封装)上 A、C 合格,B 不合格。
6.2 推荐:完整 C(按"更清晰、分层更合理、耦合更少"取舍)
抽出与 bridge 平级的
DaemonWorkspaceService(不经 bridge 委托——委托反而加耦合),HttpAcpBridge瘦身/改名为AcpSessionBridge:拆分原则 = 是否依赖 ACP child: 依赖 live child(prompt/会话/mcp·skills·preflight 状态/context)→ 留
AcpSessionBridge;纯 daemon 本地(文件/登录/agents/memory/init/tool/env)→ 进DaemonWorkspaceService。两组件互不依赖;跨切需求(如 tool-toggle 走 EventBus)用单向注入(service 持 event-publish 回调,不反向)。为什么满足三条:更清晰(各一职责、命名诚实);分层合理(L1 薄 → L2 聚焦 → L3 连接,trust/context 单点封装);耦合更少(bridge 不背工作区杂活、service 不依赖 bridge、两传输按域调聚焦接口)。
代价(已知):比 A 改动大——新建 service + 统一 context;REST 现有 file/auth/agents/memory 路由改为调 service;把 bridge 上纯本地工作区操作迁入 service;/acp 新增
_qwen/fs|auth|workspace/agent|memory方法。鉴于优先架构质量,此代价值得。7. Client 接入样例
核心:L2 拆分对 client 完全透明。 client 只连一个 L1 传输面、一套 token;daemon 内部把"会话域"路由给
AcpSessionBridge、"工作区域"路由给DaemonWorkspaceService。7.1 ACP 原生 client 走
/acp(一条连接,跨两个域)第 5–7 步无 sessionId、与会话调用共用同一连接与 conn SSE——client 感知不到它们落到不同 L2 组件。
7.2 Web/SDK client 走 REST+SSE(等价)
7.3 落点映射(client 调用 → L1 → L2)
session/new·prompt·cancel·set_config_option、SSEsession/update_qwen/workspace/agents·_qwen/fs/read·_qwen/workspace/memory·_qwen/auth/*_qwen/workspace/{mcp,skills,providers,env,preflight}、session/context7.4 SDK
DaemonClient/DaemonSessionClient(sdk-typescript/src/daemon/)把 REST+SSE 封装成方法;未来给/acp出对等 helper(工作区域用 SDKextMethod('_qwen/…'))。无论哪种,client 看到统一 API,L2 拆分始终隐藏在 L1 之后。