qwen-code 会话来源溯源:session source 元数据在生命周期 Hook 载荷中的传播机制
2026/9/14 13:10:11 网站建设 项目流程

qwen-code 会话来源溯源:session source 元数据在生命周期 Hook 载荷中的传播机制

【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code

本文基于设计文档 session-source-lifecycle-hooks.md,讲解 qwen-code 如何把 daemon 创建会话时写入_meta['qwen.session.source']sourceType/sourceId一路带入SessionStartUserPromptSubmitStopSessionEnd等生命周期事件的 Hook 载荷,并逐环节给出解析、存储、注入的源码实现证据。读完你可以理解这条溯源链在代码中的每一处落点,并能在 Hook 接收端独立完成会话来源归因(例如区分会话来自 channel、定时任务还是 standalone daemon)。

一、背景:Hook 接收端看不到会话来自哪里

daemon 的会话创建链路早已支持把可选的sourceTypesourceId转发给 ACP,落在会话参数的_meta['qwen.session.source']中。ACP runtime 目前消费 source type 做行为决策——例如为 channel 会话禁用原生 cron 调度器——但这两个值此前从未出现在生命周期 Hook 的 payload 里。

这就造成一个时序缺口:当SessionStart事件触发时,bridge 尚未把会话来源元数据持久化,Hook 接收端无法对一个新会话做出来源归因。本次设计的目标因此非常克制:在 ACP 会话边界一次性解析既有创建元数据,落到会话的Config上,再由 Hook 载荷构造器顺带透出——不引入任何新的传输通道。

二、ACP 会话边界的解析与校验

2.1 元数据键与校验规则

来源元数据的常量定义与校验逻辑集中在 session-source.ts:

export interface SessionSourceMetadata { sourceType?: string; sourceId?: string; } export const SESSION_SOURCE_META_KEY = 'qwen.session.source'; export const SESSION_SOURCE_TYPE_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/; export const MAX_SESSION_SOURCE_ID_LENGTH = 256;

解析入口parseSessionSource(sourceType, sourceId)做两级校验:

  • 两者同时缺省时直接返回空对象,视为“无来源”;
  • 提供sourceType时必须是匹配[a-z][a-z0-9_-]{0,63}的字符串;
  • 提供sourceId时必须是非空字符串、长度不超过 256、且不含控制字符(charCode ≤ 31 或 127)。

任何一项不满足都返回{ error: string }结构而不是抛出异常,由调用方决定错误处理方式。同一文件还定义了几个已知来源常量:保留的standalone来源类型,以及定时任务运行标记——任务运行子会话保持default来源类型以便和普通会话并列展示,靠sourceIdscheduled_task_run:前缀来识别。从源码结构看,这类常量被会话列表与 web-shell 侧边栏逻辑消费。

bridge 层在会话创建请求处理中完成解析,出错即拒绝请求(bridgeClient.ts):

const source = parseSessionSource(params['sourceType'], params['sourceId']); if ('error' in source) { throw RequestError.invalidParams(undefined, source.error); } // 子会话不允许再覆写 sourceType(定时任务运行类型除外): // `'sourceType' is not settable on a sub-session`

2.2 从_meta读取并写入会话 Config

ACP agent 一侧,acpAgent.ts 提供getSessionSource,从_meta[SESSION_SOURCE_META_KEY]读取元数据;sourceType不是字符串时整体视为无来源,同时透传可选的sourceIddaemonOwnedStandaloneCreation标记:

function getSessionSource(params: { _meta?: unknown }): SessionSource | undefined { const meta = isObjectRecord(params._meta) ? params._meta : undefined; const value = meta?.[SESSION_SOURCE_META_KEY]; if (!isObjectRecord(value) || typeof value['sourceType'] !== 'string') { return undefined; } return { sourceType: value['sourceType'], ...(typeof value['sourceId'] === 'string' ? { sourceId: value['sourceId'] } : {}), ...(value[DAEMON_OWNED_STANDALONE_CREATION_KEY] === true ? { daemonOwnedStandaloneCreation: true } : {}), }; }

在会话创建路径上,Config 构造完成后立即写入来源(acpAgent.ts):

if (sessionSource) { config.setSessionSource(sessionSource.sourceType, sessionSource.sourceId); }

Config侧对应的存取方法定义在 config.ts:

setSessionSource(sourceType: string, sourceId?: string): void { this.sessionSourceType = sourceType; this.sessionSourceId = sourceId; } getSessionSourceType(): string | undefined { return this.sessionSourceType; } getSessionSourceId(): string | undefined { return this.sessionSourceId; }

设计文档要求“expose read-only getters”:对外只有getSessionSourceType()/getSessionSourceId()两个读取入口,没有会话中途改写的公开方法——来源元数据在会话生命周期内是不可变的,这与“一次性解析、一次落盘”的边界设计一致。

三、Hook 载荷注入:common input builder 统一透出

3.1createBaseInput的字段映射

核心实现在 hookEventHandler.ts 的私有方法createBaseInput

private createBaseInput(eventName: HookEventName): HookInput { const transcriptPath = this.config.getTranscriptPath(); const sourceType = this.config.getSessionSourceType(); const sourceId = this.config.getSessionSourceId(); ... return { session_id: this.config.getSessionId(), ...(sourceType !== undefined ? { source_type: sourceType } : {}), ...(sourceId !== undefined ? { source_id: sourceId } : {}), transcript_path: transcriptPath, cwd: this.config.getWorkingDir(), hook_event_name: eventName, timestamp: new Date().toISOString(), permission_mode: approvalModeToPermissionMode(this.config.getApprovalMode()), ...(agentId ? { agent_id: agentId } : {}), ...(promptId ? { prompt_id: promptId } : {}), }; }

两个关键细节:

  1. 字段命名转换:进程内 camelCase 的sourceType/sourceId在 payload 中映射为 snake_case 的source_type/source_id,与 types.ts 中HookInput的既有字段风格一致;
  2. 条件对象展开...(value !== undefined ? { key: value } : {})让缺省值直接缺席,payload 中不会出现source_type: nullsource_id: ""这类空字段。这正是设计文档“Conditional object spreads omit absent values”所指的兼容手段。

3.2 所有生命周期事件获得同一份归因

因为每个fire*Event方法都先展开createBaseInputSessionStartUserPromptSubmitStopSessionEnd(以及工具、通知等其余事件)全部获得同一份来源归因,不需要任何按事件单独接线。以fireSessionStartEvent为例(hookEventHandler.ts):

async fireSessionStartEvent( source: SessionStartSource, model: string, permissionMode?: PermissionMode, agentType?: AgentType, signal?: AbortSignal, ): Promise<AggregatedHookResult> { const input: SessionStartInput = { ...this.createBaseInput(HookEventName.SessionStart), permission_mode: permissionMode ?? PermissionMode.Default, source, model, agent_type: agentType, }; ... }

由此,channel 会话的SessionStart载荷形如:

{ "hook_event_name": "SessionStart", "session_id": "<session-id>", "source_type": "channel", "source_id": "feishu-main", "source": "startup", "model": "<model>", "permission_mode": "default" }

这里有一个容易混淆的点:SessionStartInput自带的source字段是事件自身的启动来源(SessionStartSource),表示“这次启动是 startup 还是 resume”;新增的source_type/source_id描述的是“这个会话由谁创建”。两者语义独立,恰好同时出现在同一事件中,接收端不应混用。

四、边界:纯读透传,不改变其他契约

设计文档的 “Boundaries” 一节明确了这条链路的不变量——它只是对既有创建元数据的一次读透传(read-through),以下均不受影响:

  • REST 侧会话创建请求协议;
  • ACP bridge 的元数据键_meta['qwen.session.source']
  • capability negotiation 协商;
  • 会话持久化与 resume 行为。

因此唯一的可观察差异在 Hook payload:无来源元数据的会话,其 payload 与改动前完全一致;有来源的会话,payload 上只是多两个可选字段。会话未携带来源元数据时“keeps the previous hook payload shape”,这是接收端兼容性承诺。

五、验证点与测试落点

设计文档 “Verification” 一节列出的三组验证,在当前仓库中都能对应到具体测试:

  1. Hook handler 测试(hookEventHandler.test.ts):覆盖SessionStartpayload 上来源字段“存在 / 缺席”两种形态——存在时断言source_type: 'channel'source_id: 'feishu-main';缺席时用not.toHaveProperty保证字段彻底不出现而非空值:
expect(input).not.toHaveProperty('source_type'); expect(input).not.toHaveProperty('source_id');
  1. ACP session 测试(acpAgent.test.ts):用例 “stores ACP session source metadata on the session config” 覆盖 channel 来源元数据从_meta传播进会话Config的完整路径,并断言setSessionSource被以解析后的值调用。

  2. Channel worker 测试:既有的创建元数据覆盖包含“channel 实例名作为sourceId”的场景,验证 daemon 侧发送端与上述解析契约一致。

六、接收端集成要点

结合上述实现,在 qwen-code 的 Hook 接收端做来源归因时可以遵循:

  • 成对读取:以source_type+source_id作为组合键,source_type的出现本身即表示该会话声明了来源;
  • 按可选字段处理:旧版本 payload 与无来源会话都没有这两个键,不要假设其恒在;
  • 区分语义层次source_type回答“谁创建了会话”(如channelstandalone、定时任务运行),而各事件自身的字段(如SessionStartsource启动来源、Stop的结束上下文)回答“本事件为何触发”,两者在接收端逻辑中应分属不同分支。

这条机制的价值在于:Hook 生态(审计、路由、统计、按来源触发差异化动作)从此可以在SessionStart触发的第一时间拿到与 ACP runtime 相同的归因依据,而不必等待 bridge 侧的来源持久化完成。

【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询