qwen-code Telemetry 运行时客户端归因:基于环境标记的 Daemon 会话 channel 上报方案
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
本文对应仓库设计文档:telemetry-runtime-client-attribution-design.md,配套 issue 编号 #8660,方案基于 2026-08-07 对 qwen-code main 分支的代码复核。
本篇技术指南聚焦 qwen-code 的 telemetry(usage-statistics / qwen-logger RUM)中properties.channel维度的运行时归因问题:当会话经由qwen serve(daemon)承载时,如何在不新增 payload 键、不破坏现有 Schema 的前提下,准确区分 SDK daemon 客户端、Web Shell、Tauri 桌面 shell 与三方 ACP 直连等不同入口。读完本文,你将掌握 daemon 桥接模型对进程级归因的约束、QWEN_CODE_SERVE/QWEN_CODE_DESKTOP环境标记的完整设计语义、回退解析的优先级规则,以及这一方案在 sandbox 透传与项目环境文件防护中的安全边界,并能在源码与测试中逐行印证该机制的实际落点。
1. 背景:单一入口维度properties.channel的归因缺口
qwen-code 的默认 usage-statistics(qwen-logger RUM)载荷中,入口归因只有properties.channel一个维度,它直接来自 CLI 启动时的--channel标志。在非 daemon 场景下,这一维度能正确表达客户端身份:
- VS Code 伴生插件直接启动
qwen --acp --channel=VSCode; - Electron 桌面端直接启动
qwen --acp --channel=desktop; - TS / Python / Java SDK 的主入口(
query())直接 spawn CLI(stream-json 模式),自带--channel=SDK; --acp未显式指定 channel 时,回退为ACP。
但qwen serve(daemon)的 spawn 工厂启动的是不带 channel的qwen --acp子进程(见 packages/acp-bridge/src/spawnChannel.ts 中createSpawnChannelFactory对'--acp'参数的处理,命令行为cliEntry --acp [extraArgs],不含--channel)。其后果是:
经 daemon 承载的会话——SDK 的 daemon 客户端入口(如 TS SDK 的
DaemonClient/DaemonSessionClient)、Web Shell、Tauri 桌面 shell——全部上报为ACP,无法区分。
虽然 Tauri shell 启动 daemon 时已设置了QWEN_CODE_DESKTOP=1(见 packages/desktop-shell/src-tauri/src/runtime.rs 第 62 行.env("QWEN_CODE_DESKTOP", "1")),但 telemetry 从未读取该变量。
设计文档同时澄清了一个容易混淆的概念:app.channel来自~/.qwen/source.json,表达的是安装来源(该二进制从何处安装),与运行时入口归因是不同维度的信息,不应被重载。
2. 方案总览:环境标记 + 回退解析,复用既有维度
核心思路是复用现有properties.channel维度,不新增 payload 键。设计文档给出了两个前置判断:
getChannel()经复核没有任何行为消费方,只有 telemetry 读取,channel 是纯上报维度,扩展其取值无副作用;- 归因手段限定在进程级,因为 qwen-logger payload 是进程级构造的。
daemon 在每个子进程环境中设置QWEN_CODE_SERVE=1标记,覆盖两个 spawn 点:
| Spawn 点 | 文件 | 说明 |
|---|---|---|
| ACP 会话子进程 | packages/acp-bridge/src/spawnChannel.ts(createSpawnChannelFactory,第 463 行childEnv['QWEN_CODE_SERVE'] = '1') | 每个 workspace 的qwen --acp子进程 |
| channel worker | packages/cli/src/serve/channel-worker-supervisor.ts(createWorkerEnv,第 652 行env['QWEN_CODE_SERVE'] = '1') | worker 内channels/base/AcpBridge.ts通过{...process.env}继续继承该标记 |
CLI 侧的回退解析集中在 packages/cli/src/config/acp-channel-fallback.ts 的resolveAcpChannelFallback():
| 条件 | channel 取值 |
|---|---|
显式--channel=X | X(不变,显式参数优先) |
QWEN_CODE_DESKTOP=1 | desktop(Tauri shell 会话,与 Electron 桌面端同一客户端身份) |
QWEN_CODE_SERVE=1 | daemon(daemon 承载的会话) |
| 其余 | ACP(直接三方 ACP 启动,不变) |
注意两个标记的优先级:QWEN_CODE_DESKTOP的判断先于QWEN_CODE_SERVE,因为 Tauri 会话同样是 daemon spawn 的(同时携带两个标记),但 launcher 身份是更细粒度的信号,优先表达为desktop。该优先级在源码注释(acp-channel-fallback.ts 第 30-32 行)和测试中均有明确固化。
3. 源码级实现:从回退函数到配置注入
3.1 回退函数的常量与解析逻辑
packages/cli/src/config/acp-channel-fallback.ts 是整套机制的枢纽,导出两个环境变量常量和解析函数:
export const QWEN_CODE_SERVE_ENV = 'QWEN_CODE_SERVE'; export const QWEN_CODE_DESKTOP_ENV = 'QWEN_CODE_DESKTOP'; export function resolveAcpChannelFallback( env: NodeJS.ProcessEnv = process.env, ): string { // The desktop marker wins: Tauri sessions are daemon-spawned too, but the // launcher identity is the finer-grained signal. if (env[QWEN_CODE_DESKTOP_ENV] === '1') { return 'desktop'; } if (env[QWEN_CODE_SERVE_ENV] === '1') { return 'daemon'; } return 'ACP'; }文件头部的注释揭示了设计动机:qwen --acp子进程无法自行判断自己是否由 daemon spawn——VS Code 伴生、Electron 桌面端、三方 ACP 集成 spawn 的是完全相同的命令行,唯一的区分手段就是环境标记。
3.2 在 CLI 配置解析中的应用点
回退函数在 packages/cli/src/config/config.ts 第 950-956 行的 ACP fallback 逻辑中被调用:
// Apply ACP fallback: if acp or experimental-acp is present but no explicit // --channel, attribute the launch — daemon-spawned children carry the serve // marker, the Tauri desktop shell additionally sets QWEN_CODE_DESKTOP. if ((result['acp'] || result['experimentalAcp']) && !result['channel']) { (result as Record<string, unknown>)['channel'] = resolveAcpChannelFallback(); }即:仅当--acp(或已弃用的--experimental-acp)存在且未显式传--channel时才触发回退,显式参数始终优先,保证既有取值(VSCode/desktop/SDK/ worker 名)语义不变。
3.3 daemon 侧的两个标记注入点
ACP 子进程(spawnChannel.ts):createSpawnChannelFactory在spawn()前构造childEnv,先经scrubChildEnv剔除 denylist 键,再写入QWEN_CODE_NO_RELAUNCH与QWEN_CODE_SERVE:
childEnv['QWEN_CODE_NO_RELAUNCH'] = 'true'; // Marks the child as daemon-spawned so its ACP channel fallback reports // channel=daemon in usage statistics (see cli/src/config/acp-channel-fallback.ts). childEnv['QWEN_CODE_SERVE'] = '1';channel worker(channel-worker-supervisor.ts):createWorkerEnv基于 base env 构造 worker 环境,同样写入标记,并继续携带QWEN_DAEMON_URL_ENV/QWEN_DAEMON_WORKSPACE_ENV等 daemon 上下文:
env['QWEN_CODE_NO_RELAUNCH'] = 'true'; // Marks the worker (and the ACP children it spawns) as daemon-spawned so // the ACP channel fallback reports channel=daemon in usage statistics // (see cli/src/config/acp-channel-fallback.ts). env['QWEN_CODE_SERVE'] = '1';worker 内的channels/base/AcpBridge.ts通过{...process.env}展开继承环境,因此 worker 再 spawn 出的 ACP 子进程会继续携带QWEN_CODE_SERVE=1,归因链不断裂。
3.4 Tauri 桌面 shell 的标记来源
packages/desktop-shell/src-tauri/src/runtime.rs 的DesktopRuntime::start在构造Command时设置:
.env("QWEN_CODE_DESKTOP", "1") .env("QWEN_SERVER_TOKEN", &token);daemon 子进程继承该标记,因此 Tauri shell 承载的会话在回退解析时命中第一分支,上报desktop——与 Electron 桌面端显式传--channel=desktop的客户端身份保持一致。
4. 归因矩阵:各场景的最终上报值
设计文档给出了完整的归因矩阵,这是本方案的行为契约:
| 场景 | properties.channel |
|---|---|
| 交互/无头 CLI | (无) |
| 三方直接 ACP | ACP |
| VS Code 伴生 | VSCode |
| Electron 桌面端(直连,不走 daemon) | desktop |
SDKquery()直连(TS/Python/Java,不走 daemon) | SDK(SDK 自带,不变) |
| daemon 会话(SDK daemon 客户端、Web Shell 等) | daemon |
| daemon 会话(Tauri desktop shell) | desktop |
| docker/podman sandbox 内的 daemon 会话 | 继承外层daemon/desktop |
| daemon channel worker | worker 名(如 feishu,不变) |
可见本方案只改变了 daemon 承载场景的取值(由错误的ACP修正为daemon或desktop),其余场景全部保持不变。
5. 为什么 daemon 会话不能像 VS Code 那样直接传--channel
一个自然的疑问是:VS Code 伴生能传--channel=VSCode,daemon 为什么不能直接传--channel=daemon?设计文档从桥接模型层面给出了三条硬约束:
- VS Code 伴生自己拥有 spawn:一个客户端 = 一个专属子进程,所以进程级参数能精确表达客户端身份;
- daemon 是共享进程模型:
packages/acp-bridge/src/bridge.ts中,一个 bridge(一个 workspace)至多一个qwen --acp子进程,所有客户端的会话经connection.newSession()多路复用到同一个进程上,共享进程 / OAuth / FileReadCache; - 因此:
- spawn 时不知道哪个客户端会连进来(子进程可能预热);
- 同一进程内同时跑着不同客户端的会话,进程级参数无法表达会话级身份;
- qwen-logger payload 是进程级构造的,进程级 channel 无法按会话区分客户端。
结论:环境标记 + 回退解析是当前模型下唯一的进程级归因手段——spawn 时无法预知客户端,但可以确定"这是 daemon 生的孩子",把信息从 spawner 传递到被 spawn 的进程中。
6. Schema 影响、兼容性与安全边界
6.1 Schema 与既有取值
- 零新增键:
properties.channel仅新增一个可能取值daemon(desktop是既有取值,Tauri shell 会话并入其中); app.channel语义与取值不变;显式--channel的既有取值(VSCode/desktop/SDK/ worker 名)不变;QWEN_CODE_SERVE为信息性标记,不含敏感信息;它不进入SCRUBBED_CHILD_ENV_KEYSdenylist(见 spawnChannel.ts 第 694-698 行,该集合仅含QWEN_SERVER_TOKEN、QWEN_CODE_SIMPLE、EXTERNAL_TOOL_GUARD_TOKEN_ENV),denylist 语义不受影响。
6.2 sandbox 透传
docker/podman sandbox 会显式透传QWEN_CODE_SERVE/QWEN_CODE_DESKTOP,避免 sandbox 内的子进程因丢失标记而退回ACP。实现见 packages/cli/src/serve/sandbox.ts 的getSandboxPassthroughEnvArgs(),它在 passthrough 清单中同时列出QWEN_CODE_SERVE_ENV与QWEN_CODE_DESKTOP_ENV,仅在宿主环境存在对应变量时生成--env KEY=VALUE参数:
export function getSandboxPassthroughEnvArgs( env: NodeJS.ProcessEnv = process.env, ): string[] { return [ 'QWEN_DEBUG_LOG_FILE', 'QWEN_CODE_LEGACY_MCP_BLOCKING', SKIP_UPDATE_CHECK_ENV_VAR, CUSTOM_SANDBOX_IMAGE_ENV_VAR, HOST_UPDATE_RELAUNCH_ENV_VAR, QWEN_CODE_SERVE_ENV, QWEN_CODE_DESKTOP_ENV, ].flatMap((envVar) => env[envVar] === undefined ? [] : ['--env', `${envVar}=${env[envVar]}`], ); }6.3 防止伪造归因:项目环境文件禁止设置标记
为了让上报数据可信,项目.env/settings.env不能设置这两个标记,避免 workspace 伪造客户端归因。该防护在 packages/cli/src/config/shared-env-keys.ts 的PROJECT_ENV_HARDCODED_EXCLUSIONS中实现——两个标记被列入硬编码排除清单,注释明确写道:
Runtime attribution markers are stamped by trusted launchers. A project
.envmust not spoof client channel telemetry.
即:归因标记只能由受信任的 launcher(daemon spawn 工厂、channel worker supervisor、Tauri runtime)盖章,仓库内容不得自行声明。
7. 测试验证:行为契约的固化
该方案的行为契约在多个测试文件中被固化:
- packages/cli/src/config/acp-channel-fallback.test.ts 覆盖
resolveAcpChannelFallback的全部分支:- 无标记直接启动 →
ACP; - 仅
QWEN_CODE_SERVE=1→daemon; - 仅
QWEN_CODE_DESKTOP=1→desktop; - 两个标记同时存在 →
desktop(launcher 身份优先); - 标记值为
''/'0'/'false'时一律忽略 →ACP(=== '1'严格判断);
- 无标记直接启动 →
- packages/acp-bridge/src/spawnChannel.test.ts 第 236 行断言 spawn 子进程的 env 中
QWEN_CODE_SERVE为'1'; - packages/cli/src/serve/channel-worker-supervisor.test.ts、packages/cli/src/serve/sandbox.test.ts 分别覆盖 worker 环境构造与 sandbox 透传行为;
- packages/cli/src/config/shared-env-keys.test.ts 校验项目
.env排除清单包含两个标记。
这些测试共同构成回归防线,任何对回退优先级或标记注入点的改动都会在 CI 中被捕获。
8. 后续工作:从进程级归因走向会话级归因
设计文档明确划定了本次范围的边界:按 SDK / Web Shell细分客户端需要会话级归因,不在本次范围内。其设想路径是:
- 各客户端在创建会话时经由 daemon 自我声明(self-declare);
- 现有
qwen.session.sourcemeta →config.setSessionSource(sourceType, sourceId)管道是自然的扩展点——该管道在 packages/core/src/config/config.ts 第 4617-4620 行实现,为 #8155 引入的 session lifecycle 钩子配套; - 当前 Web Shell 主会话仅使用
sourceType: 'default'; - 由于默认 payload 是进程级的,会话级身份落到 telemetry 还需要会话维度的支持,届时再决定是否引入独立的 client 键。
简言之:本方案解决的是"这个进程从哪来"(进程级),后续工作要解决的是"这个会话属于哪个客户端"(会话级)——后者依赖 daemon 的会话生命周期管道,属于独立的设计演进。
9. 小结
QWEN_CODE_SERVE/QWEN_CODE_DESKTOP环境标记方案,以零新增 payload 键、零行为语义改动、两处 spawn 注入点 + 一处回退解析的最小代价,修复了 daemon 承载会话全部误报为ACP的归因缺口,使daemon、desktop、worker 名在 usage-statistics 中得以区分。它的设计价值在于:在不改变 daemon 共享子进程桥接模型的前提下,用 spawner 盖章、子进程自报的方式,把"进程由谁而生"这一事实可靠地带入 telemetry,并通过 sandbox 透传保持标记在容器边界的连续性、通过项目环境文件排除清单阻断伪造路径。对后续要接入 telemetry 归因的客户端或通道实现,本文列出的源码、常量与测试即是最直接的参考契约。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考