qwen-code Daemon 多工作区 Phase 4b:Channel Workers 按工作区分组的设计与实现
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
本篇技术指南围绕 qwen-code 中 daemon(qwen serve后台服务)的多工作区能力展开,聚焦 Phase 4b 的 channel-worker 切片:如何把原本绑定在主工作区(primary workspace)的单实例 channel worker,升级为“每个注册且受信任的工作区各拥有一个独立 worker 进程”的分组模型。读完本文,你将掌握resolveChannelWorkspaceGroups的分组判定规则、ChannelWorkerGroup组管理器的生命周期语义、pidfile 与 daemon status 的兼容性演进,以及多工作区模式下的启动时序、错误码与运维约束,可直接用于理解qwen serve --channel在多工作区场景下的行为与排障。
背景:为什么 channel worker 需要按工作区分组
在引入多工作区能力之前,qwen serve --channel <name>只会启动一个channel worker,且该 worker 严格绑定到 daemon 的主工作区(primary workspace):worker 以主工作区的 cwd 作为工作目录,并携带QWEN_DAEMON_WORKSPACE指向主工作区。这意味着所有 channel 的会话、配置合并、.env环境覆盖都发生在同一个工作区上下文里。
当 daemon 进入多工作区模式(对应 daemon-multi-workspace-phase2a-sessions.md 以来的系列设计)后,不同工作区各自拥有独立的会话路由与env.effectiveEnv环境覆盖。若 channel worker 仍然只绑定主工作区,就会出现三种不一致:
- 配置归属不清:channel 配置通过
loadSettings(W).merged.channels按 system / user / workspace 三级作用域合并,一个 channel 名可能同时出现在多个工作区的合并配置里,无法靠“名字在哪个工作区”判定归属。 - 环境缺失:
channel-worker-supervisor.ts原先以{...process.env}作为 worker 环境基座;在多工作区模式下父进程环境是 daemon 的基础环境(Phase 2a 的环境隔离产物),非主工作区的 worker 会丢失该工作区自己的.env。 - 校验错位:
commands/channel/daemon-worker.ts中的validateChannelWorkspaces强制要求每个 channel 解析出的 cwd 必须等于 daemon 工作区(即主工作区),非主工作区的 channel 会直接抛错。
Phase 4b 的设计目标因此非常明确:每个注册且受信任的工作区,拥有一个绑定到该工作区 cwd、QWEN_DAEMON_WORKSPACE与有效环境覆盖的独立 worker 进程,同时保持单工作区行为完全不变、旧 pidfile/状态读取方不受影响。
分组模型:channel 按其解析后的 cwd 隐式归属
设计文档给出的映射模型是“隐式按 resolved cwd 分组”——不引入任何新的 CLI 语法。一个 channel 归属于某个注册工作区W,当且仅当它的配置 cwd 解析并规范化后等于W。
归属判定的四种情形
对每个被选中的 channel 名name,遍历注册工作区集合W,若name出现在loadChannelsConfig(W)中,则计算resolvedCwd = canonicalizeWorkspace(resolvePath(cfg[name].cwd ?? W))。W成为候选归属者,当且仅当resolvedCwd === W(即该 channel 在W下能通过validateChannelWorkspaces校验):
| 配置情形 | 归属结果 | 说明 |
|---|---|---|
显式cwd= 某个已注册路径 X | 唯一归属 X | 无歧义 |
无cwd,且仅在某个工作区自己的作用域定义(如/B/.qwen/settings.json) | 唯一归属 B | 该 channel 只出现在 B 的合并配置中,解析回 B |
无cwd,在 user/system 作用域定义 | 多个归属者 | 每个W都能满足条件,真正有歧义,需要 operator 处置 |
显式cwd= 未注册路径 | 零归属者 | 没有任何W满足条件 |
错误与聚合
- 零归属者→ 错误码
channel_workspace_mismatch:channel 未配置,或其cwd指向未注册的工作区。 - 多于一个归属者→ 错误码
ambiguous_channel_workspace:典型是 user/system 作用域下没有cwd的 channel;operator 必须把它限定到某个工作区的作用域,或补一个显式cwd。 - 归属者不受信任→ 错误码
untrusted_workspace:channel 需要在该工作区创建会话,受信任是硬前提。 - 唯一且受信任的归属者→ 按归属者聚合 channel 名,每组生成
{mode:'names', names}选择。 mode:'all'→ v1 阶段保持 primary-only:生成[{ workspaceCwd: primary, selection:{mode:'all'} }];主 worker 加载主工作区的合并 channel,cwd 非主工作区的条目继续走原有validateChannelWorkspaces报错路径。- 单工作区(仅主工作区)→
resolvedCwd只可能为主工作区,产出与今天完全相同的单一分组。
源码实现对照
这一算法在 channel-workspace-grouping.ts 中落地为纯函数resolveChannelWorkspaceGroups,其输入ResolveChannelWorkspaceGroupsInput通过注入loadChannelsConfig(workspaceCwd)保持纯函数可单测。关键点:
- 共享 cwd 辅助函数
resolveChannelOwnerCwd(rawCwd, workspaceCwd)先做resolveChannelCwd(显式绝对路径与~/...保持原义,普通相对路径以“正在加载设置的工作区”为基准解析),再做canonicalizeWorkspace规范化,保证 serve 层分组与 worker 自身校验对归属的判定永远不会不一致。 - 工作区注册表(registry)只保留分组所需字段(
workspaceCwd、primary、trusted、provenance),并过滤掉provenance === 'live-conversation'的运行时。 - 分组阶段先为每个工作区加载一次合并配置缓存(
channelsConfigByWorkspace),避免按 channel 名重复加载。 - 当某个 channel 的显式
cwd无法规范化(如 EACCES)时,该工作区视为非归属者,落入零归属者错误路径,由channel_workspace_mismatch兜底。
单元测试 channel-workspace-grouping.test.ts 覆盖了四种典型场景:resolveChannelOwnerCwd缺省 cwd 回落加载工作区、显式 cwd 独立于加载工作区做规范化、相对路径相对加载工作区解析;resolveChannelWorkspaceGroups拒绝无主工作区的注册表、将显式 cwd channel 钉到目标工作区(即使同名条目在 user 作用域对所有工作区可见)、将 home-relative channel 归到注册工作区、将仅存在于工作区作用域的 cwdless channel 归到该工作区。
Worker 身份与环境:从process.env到每工作区环境覆盖
单 worker 时代,channel-worker-supervisor.ts以{...process.env}构建 worker 环境。多工作区模式下父进程环境是 daemon 基础环境,非主工作区 worker 会丢失该工作区自己的.env,因此设计引入可选参数workerBaseEnv:
CreateChannelWorkerSupervisorOptions新增可选workerBaseEnv(默认process.env)。createWorkerEnv以workerBaseEnv ?? process.env为基座;其余逻辑完全不变:注入QWEN_DAEMON_WORKSPACE、token 环境变量清洗、daemon token 注入。- 组管理器传入
runtime.env.effectiveEnv ?? process.env:直接从字段读取,避免引入server.ts的私有辅助函数;单工作区的 parent-process-mode 运行时effectiveEnv为undefined,正好回落到process.env,与今天行为一致。
对应源码位置:
- channel-worker-supervisor.ts 中
workerBaseEnv的注释明确说明其用途:多工作区模式下调用方传入归属运行时的有效环境覆盖,使 worker 继承该工作区的.env而非 daemon 基础环境。 - channel-worker-supervisor.ts 的
createWorkerEnv负责写入CHANNEL_DAEMON_WORKER_SENTINEL、QWEN_DAEMON_URL_ENV、QWEN_DAEMON_WORKSPACE_ENV,删除QWEN_SERVER_TOKEN_ENV、QWEN_DAEMON_TOKEN_ENV、EXTERNAL_TOOL_GUARD_TOKEN_ENV等继承 token,再按需注入 daemon token——保证 worker 进程不会把宿主环境里的服务端 token 泄漏给被代理的模型侧调用。
daemon-worker 校验修复:从workspaceCwd到workspaces注册表
worker 侧的commands/channel/daemon-worker.ts原先只认capabilities.workspaceCwd(主工作区),非主工作区的 worker 直接抛错。Phase 4b 的修复如下:
DaemonCapabilitiesLike新增可选workspaces?: Array<{ cwd; id; primary; trusted }>(该字段自 Phase 2a 起就由/capabilities发布,此处补上类型声明)。- 校验逻辑先做
daemonWorkspace = canonicalizeWorkspace(opts.workspace);当capabilities.workspaces存在时,要求匹配其中一个注册工作区且为受信任,否则回落到旧的== capabilities.workspaceCwd单工作区检查。 - 两侧都是规范化后的(supervisor 传入
runtime.workspaceCwd),比较稳定。 - worker 其余部分(channel 配置加载、
validateChannelWorkspaces、createOrAttach({workspaceCwd}))本已支持多工作区路由,无需改动。
源码位置:daemon-worker.ts 定义DaemonCapabilitiesLike.workspaces;daemon-worker.ts 实现分支:多工作区 daemon 校验注册与信任,旧版单工作区 daemon 走workspaceCwd兼容路径;daemon-worker.ts 的validateChannelWorkspaces继续以规范化 cwd 与 daemon 工作区做一致性校验,作为 worker 内部的最后防线。
Supervisor 组管理器:ChannelWorkerGroup
薄封装ChannelWorkerGroup内部持有Map<workspaceId, ChannelWorkerSupervisor>,是设计文档中“组管理器”的落地实现,源码见 channel-worker-group.ts:
- 构建:由分组结果与注册表生成,每个 supervisor 绑定其运行时的
workspaceCwd、选择(selection)与env.effectiveEnv,并通过与单 worker 相同的可注入工厂createSupervisor创建。 start():顺序启动各 supervisor;若后续某个启动失败,回滚所有已启动的 worker(stopEntriesBestEffort(started)后重新抛错)。stop():等待进行中的 reconcile(重启事务)结束,再停止所有 supervisor;killAllSync()作为信号处理器兜底,同步杀光所有(含 pending 的)worker。reconcile():daemon 级重载事务。并发请求合并(reconciling去重);supervisor 顺序重启;任何失败都会把整个组停掉(必要时先恢复已停止的旧 worker),避免出现“半套重载”的中间态。失败时抛出ChannelWorkerReconcileError,携带rolledBack、rollbackError、stopFailed与启动失败明细。snapshots():返回按工作区注解的快照ChannelWorkerSnapshot & { workspaceId; workspaceCwd; primary };primarySnapshot()支撑旧的单 worker 字段。- 路由与生命周期扩展:
deliverChannelMessage/enqueueWebhookTask按 channel 名(可选按workspaceCwd精确定位)路由到归属 supervisor,工作区排空(drain)期间投递会得到channel_worker_unavailable错误;removeWorkspace/restoreWorkspace/beginWorkspaceDrain/cancelWorkspaceDrain支撑工作区生命周期管理(对应 Phase 5 的动态增删前身)。 - 任何 supervisor 的
onReady/onExit都会触发一次全量 pidfile 重写(见下一节),且通过generation编号过滤过期回调,避免旧代 worker 的 ready/exit 事件污染新代快照。
组容量有硬性上限:assertChannelControlWorkspaceCapacity在创建与 reconcile 时对所有者数量做校验,channel-worker-group.test.ts 中即有“26 个初始 owner 在构造 supervisor 前即被拒绝”的测试。
pidfile schema 与并发安全:全量快照原子重写
单 worker 时代的ServiceInfo是channels[] / servePid? / workerPid?单条记录;daemon status 的runtime.channelWorker也是单快照。多 worker 场景下两个问题随之而来:
- N 个 worker 的
onReady/onExit并发触发:若逐条 read-modify-write,必然丢更新。解决方式是写入方从组管理器取全量快照,执行一次同步的全量重写;writeServeServiceInfo使用同步openSync/writeSync、无await,因此“最后一次写入总是持有完整图景”,具备足够的原子性。 - 旧读者兼容:
ServiceInfo新增可选workers?: Array<{ workspaceId?; workspaceCwd?; channels: string[]; workerPid? }>;顶层channels变成所有 worker 的 channel 并集,顶层workerPid保留为主 worker 的 pid,qwen channel status这类只读workerPid与channels的旧读者不受影响。parseServiceInfo对workers?做可选校验并透传。
写入侧复用既有O_RDWR + O_NOFOLLOW与 serve 所有权保护语义,保证 pidfile 不因符号链接攻击被篡改。
daemon status schema:channelWorkers快照数组
DaemonStatusRuntime新增可选channelWorkers?: Array<ChannelWorkerSnapshot & { workspaceId; workspaceCwd; primary }>,必填字段channelWorker继续保留为主分组快照,供旧客户端使用。取数函数getChannelWorkerSnapshots从run-qwen-serve经ServeAppDeps与BuildDaemonStatusOptions一路注入(镜像既有getChannelWorkerSnapshot路径),并在 bootstrap 状态中一并暴露;组创建前(pre-startup)报告 disabled 快照。
对应源码:run-qwen-serve.ts 中getChannelWorkerSnapshots与getChannelWorkerSnapshot相邻定义(后者回落到 disabled 快照),状态装配处同时输出channelWorker与channelWorkers两个字段;channel-worker-manager.ts 的ChannelWorkerManagerState也通过workers: ChannelWorkerGroupSnapshot[]对外暴露分组快照。
编排与启动时序:fail-fast 分组 + 收敛点创建
启动编排是多工作区改造中最容易出错的环节,设计做了两处关键安排:
- 早失败(fail-fast):在 listen 回调阶段、
buildRuntime之前,纯分组函数对workspaceInputs+loadSettings+ 启动期冻结的信任状态(getWorkspaceTrustStatus)执行一次分组。未知工作区、歧义、不受信任、无效 cwd 归属都会在可用句柄暴露前拒绝启动。解析出的分组计划对后续启动冻结——settings 不会在另一个文件系统快照下被重新分组。 - 收敛点创建:实际创建/启动移到
completeRuntimeStartup(所有运行时启动路径的单一收敛点:eagerdeps.bridge路径与startRuntime -> buildRuntime路径都会经过它;deps.bridge仅限单工作区,多工作区必然走startRuntime)。它从runtimeApp.locals.workspaceRegistry读取注册表(多工作区必然存在),按冻结分组逐组构建 supervisor 并启动,取代原先的单个channelWorker.start()。
时序上有几个值得注意的约定:
- 新建的 runtime app 会先发布并挂接到 ACP transports,然后才启动 channel supervisor——worker 在 bootstrap 阶段需要访问运行时
/capabilities路由,连接后可能立刻收到 channel 流量,因此其 daemon 会话路由必须已就绪。这与main分支上既有的单工作区顺序一致;runtimeReady仍要等每个被请求的 supervisor 达到 ready 才 settle。 - channel worker 启动失败依然致命:先撤回 runtime 发布,再依次拆除组、pidfile、bridges 与 listener;worker 阶段的 runtime 启动超时走同一路径,不会留下一个仍在监听的 daemon。组取消机制也能防止 teardown 开始后某个后续工作区 supervisor 再被拉起。
- pidfile 预留聚合的 channel 名;关闭路径(
stopChannelWorkerAfterFailedStartup、killAllSync、正常关闭)都扇出到组。
回归风险控制:单工作区下创建时机从 listen 回调挪到completeRuntimeStartup,既有 run-qwen-serve.test.ts 的 channel 测试(注入工厂、pidfile-on-ready、第二信号强杀)必须保持绿色;多工作区编排测试还会从 supervisor 启动时探测真实 daemon/capabilities路由,确保 runtime/worker 顺序不会退化成“只验证注入的 ready-only 工厂”。
启动行为与兼容性约束
各模式的启动行为
- 单工作区:与今天完全一致。
- 多工作区 +
--channel names:按归属者分组,每个受信任工作区一个 worker;零归属者 / 多归属者 / 不受信任 → 明确启动报错,绝不半启用。 - 多工作区 +
--channel all:仅主工作区 worker,stderr 提示非主工作区 channel 未被托管。
对 operator 的指引
要把 channel 托管到非主工作区,二选一:
- 在该工作区自己的
.qwen/settings.json中定义 channel(无需cwd,天然归属该工作区); - 在任意作用域定义,但显式给出
cwd且等于该工作区路径。
user/system 作用域且无cwd的 channel 必须在多工作区模式下被消歧(限定作用域或补cwd),否则 daemon 启动即报错。
v1 已知限制
- 歧义/同名 channel 需要未来的显式语法(设计文档的 Open questions 之一即
--channel <workspace>:<name>)。 --channel all仅主工作区。- 单 daemon 故障半径覆盖所有工作区的 worker;单一 daemon token 覆盖所有工作区。
未决问题与范围外
设计文档明确留有两个开放问题:歧义 channel 是否应通过显式--channel <workspace>:<name>语法消歧,而非直接启动报错;--channel all未来是否应扇出到所有工作区。同时,voice/workspaces/:workspace/voice/stream的每工作区化属于 Phase 4b 的独立切片、工作区动态增删属于 Phase 5,均不在本文范围内。
小结
Phase 4b channel-workers 切片的核心设计可以概括为三条主线:用 resolved cwd 做隐式归属判定(不新增 CLI 语法、与 worker 侧校验共享规范化路径)、用ChannelWorkerGroup做多 supervisor 的统一生命周期管理(顺序启动、失败回滚、并发 reconcile 合并、全量 pidfile 重写)、用“加字段、保旧值”的 schema 策略维持兼容(ServiceInfo.workers、DaemonStatusRuntime.channelWorkers均为可选新增,channels/workerPid/channelWorker继续服务旧读者)。这三条主线在 channel-workspace-grouping.ts、channel-worker-group.ts、channel-worker-supervisor.ts 与 daemon-worker.ts 中均有完整实现与单测覆盖,是理解 qwen-code 多工作区 daemon 架构时值得精读的一组源码。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考