qwen-code 扩展管理 V2:基于 extension-store 的原子事务、多工作区激活与兼容性设计
2026/9/15 15:30:39 网站建设 项目流程

qwen-code 扩展管理 V2:基于 extension-store 的原子事务、多工作区激活与兼容性设计

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

导读

本文围绕 qwen-code 仓库中 docs/design/extension-management-v2.md 这一设计文档展开,系统讲解该开源终端 AI 编程代理的新一代扩展管理协议:以QWEN_HOME/extensions下的单一用户级工件为资源模型、以ExtensionStore为唯一写入者、通过 prepare/commit 事务与单调 generation 保证扩展安装、更新、激活的原子性与跨进程一致性。读完本文,你将掌握 V2 的目录布局与锁模型、V1 迁移与降级投影机制、Daemon REST API 全貌与操作语义(202/Location/Retry-After、FIFO 队列、敏感设置 bundle、下载上限),以及客户端如何通过能力标签(capability)实现向后兼容的调用方式。

设计定位:协议 v1 之上的增量能力

Extension Management V2 并不是一次推翻重来的协议升级,而是在 daemon 协议v1之上以**增量能力(additive capability)**方式引入的扩展管理能力。设计文档明确说明:

  • 新增能力标签为extension_management_v2,客户端必须显式检查该标签,neither daemon mode nor another workspace capability implies this API——即 daemon 的运行模式或其他 workspace 能力的存在,都不能推断出 V2 扩展管理 API 可用;
  • 已经发布的workspace_extensions能力与/workspace/extensions/*路由仍然保留,作为主工作区(primary-workspace)的兼容适配器;
  • 曾经提出但被放弃的workspace_qualified_extensions方案不属于本协议

从源码看,能力标签在 packages/cli/src/serve/capabilities.ts 中统一声明:workspace_extensions: { since: 'v1' }extension_management_v2: { since: 'v1' }并列,且注释明确说明 V2 是"Global extension catalog/mutations plus workspace-qualified activation projections",对 legacy 主工作区workspace_extensions契约是**增量(additive)**关系。与之配套的还有三个相关能力标签:

  • extension_batch_activation_v2:批量激活所需;因为较老的 V2 daemon 只暴露单数(singular)激活路由;
  • extension_activation_explicit_refresh:当该标签出现时,激活提交不会刷新活动会话,客户端需在激活操作提交后单独发起刷新操作;较老 daemon 则在激活操作内部就完成刷新;
  • extension_stateextension_git_credentialsextension_local_path_install等相邻标签覆盖扩展状态、Git 凭据与本地路径安装面。

资源模型:一个工件 + 激活即策略

V2 的核心资源模型可以浓缩为一句话:一个已安装的扩展是QWEN_HOME/extensions下的一个用户级工件(artifact),激活(activation)是策略(policy),而不是该工件的第二份拷贝。

激活状态的判定顺序(优先级从高到低)为:

  1. 精确的工作区覆盖(exact workspace override):值为enableddisabled
  2. 内部精确inherit掩码:在迁移旧版路径规则(V1 path rule)时创建的内部记录,用于保住"DELETE 即继承全局默认值"的语义;
  3. 有序的 V1 路径规则(ordered V1 path rule):来自旧版extension-enablement.json的按顺序匹配规则;
  4. 全局默认(global default)

工作区身份使用 daemon 的规范化工作区路径(canonical workspace path);工作区路由先按 workspace id 选择已有运行时,其次按规范化 cwd 选择。权限模型上:

  • 读取(读投影、读目录)允许**不受信任(untrusted)**的运行时执行;
  • 激活变更、刷新、以及工作区作用域内的安装要求**受信任(trusted)**的目标;
  • 全局变更使用 daemon 常规的变更认证与安装许可(mutation authentication and install consent),与发起请求的工作区的信任状态无关

这保证了"查看扩展状态"是低权限操作,而"改变全局激活/安装"始终走严格的认证与许可流程,避免某个不受信任工作区借道扩展管理接口越权。

存储与事务边界:ExtensionStore 作为唯一写入者

单一写入者原则

ExtensionStore是扩展最终目录与 V2 激活状态的唯一写入者(only writer)ExtensionManager仍然是面向工作区的门面(facade),但 CLI、TUI、自动更新(auto-update)、daemon 以及 SDK 支撑的操作,全部委托给 store 执行变更。也就是说,所有可能改变扩展工件或激活状态的路径,最终都收敛到同一个事务入口,避免多方并发写造成的状态分叉。

目录布局

设计文档给出的布局(~/.qwen/QWEN_HOME的默认值,实际路径以QWEN_HOME环境变量为准,见 extension-store.test.ts 中对QWEN_HOME的 stub 用法):

~/.qwen/ ├── extensions/ └── extension-store/ ├── lock ├── state.json ├── state.previous.json ├── staging/ ├── rollback/ └── transactions/
  • extensions/:最终工件目录,同时存放旧版extension-enablement.json(V1 迁移来源);
  • extension-store/lock:跨进程文件锁(proper-lockfile);
  • extension-store/state.json:V2 权威状态,包含单调递增的 generation;
  • extension-store/state.previous.json:上一次状态快照,用于恢复/对比;
  • staging/:安装/更新准备的暂存区;
  • rollback/:提交前旧工件移动到此处的回滚区;
  • transactions/:事务日志(journal)目录。

在源码 extension-store.ts 中,ExtensionStore构造器默认将storeDir解析为Storage.getGlobalQwenDir()/extension-storeenablementPath解析为extensions/extension-enablement.json、状态文件为state.jsonstate.previous.json、锁文件为lock,与设计文档完全对应。

锁与单调 generation

Store 与工件共享同一文件系统,因此工件替换采用**目录重命名(directory rename)**实现,无需拷贝大目录。并发控制分两层:

  • 进程内互斥锁(in-process mutex)+proper-lockfile文件锁:序列化所有 V2-aware 进程的提交;
  • 每次变更都在持锁状态下重读状态,并把一个单调递增的 generation加一,防止丢失更新(lost updates)。

extension-store.test.ts中有一条跨进程测试用例——"serializes mutations from two Node processes sharing QWEN_HOME"(extension-store.test.ts),正是对"两个进程共享同一个QWEN_HOME时仍能串行化变更"这一保证的验证。

prepare → commit 的事务生命周期

安装/更新的准备(preparation)阶段在最终工件目录之外进行。提交(commit)流程为:

  1. 写入prepared日志(journal);
  2. 把旧工件移动到rollback/
  3. staging/中的新工件移动到位(目录重命名);
  4. 原子地写入state.json——这次状态文件重命名就是提交点(commit point)

提交点前后语义截然不同:

  • 提交点之前:任何失败都走**回滚(rollback)**恢复;
  • 提交点之后:恢复只做投影(projection)补全与清理,绝不回滚已提交的策略。

因此,一次运行时的刷新失败绝不会导致已提交的策略被回滚(A committed policy is never rolled back because one runtime refresh failed)。如果提交前操作与其回滚双双失败,调用方会同时收到两个错误,journal 保留以便 fail-closed 恢复;store 不会在工件状态歧义时继续写入。

安全细节还包括:

  • store 文件使用仅属主权限(owner-only permissions)原子 no-follow 写入
  • 对扩展 id、直接子工件路径、事务路径与名称做校验
  • 失败信息以**脱敏凭据(credential-redacted)**的来源上报,避免泄露敏感信息。

V1 迁移与降级投影

首次迁移:导入有序规则而非物化覆盖

第一个 V2-aware 进程启动时,会从extension-enablement.json导入有序规则(ordered rules),但不会把当前已注册工作区集合物化为精确覆盖(exact overrides)。也就是说,迁移保持最小侵入:旧规则被导入为 V1 路径规则层,而不是把每个工作区当前生效值固化成写死的 override。

双向投影与哈希比较

每次状态提交后,V2 会写出一个兼容投影(compatible projection),并将其哈希存入state.json。当检测到哈希不一致时,修改顺序决定恢复方向:

  • 若投影早于V2 状态(投影较旧):从权威的 V2 状态修复投影;
  • 若投影晚于V2 状态(投影在 V2 状态之后被修改):视为降级二进制(downgraded binary)的顺序写入,以新 generation重新导入

设计文档明确声明:并发 V1 与 V2 写入者共享同一个QWEN_HOME是不被支持的(列于 Non-goals),单向降级是唯一受支持的过渡路径。

inherit掩码:保住 DELETE 语义

清除一个公开工作区覆盖(public workspace override)通常意味着删除精确记录。但如果删除后较旧的路径规则会让生效值发生变化,store 会写入一个内部inherit掩码,使得DELETE仍然表示"继承全局默认值"。从源码看,WorkspaceActivation类型包含'inherit'分支(extension-store.ts),且 store 在清理覆盖时会显式写入'inherit'(如 extension-store.ts 中workspace: 'inherit'的写入路径)。这是对用户直觉的精确保护:我删除了某工作区的专属设置,它就该回到"跟全局走",而不是被一条看不见的旧规则接管。

Daemon API 全貌

全局扩展面(Global surface)

GET /extensions PUT /extensions/activation POST /extensions/install POST /extensions/check-updates POST /extensions/:extensionId/update DELETE /extensions/:extensionId PUT /extensions/:extensionId/activation GET /extensions/operations/:operationId

安装端点要求显式同意(consent)与初始激活(initial activation),初始激活类型为:

type InitialActivation = | { scope: 'user' } | { scope: 'workspace'; workspaceId: string };

即安装时可以决定是"用户级全局激活"还是"针对某个具体工作区激活"。安装源支持:

  • HTTPS Git;
  • GitHub Release;
  • npm;
  • 绝对路径本地源(absolute-path local source)

而 SSH 与 link 源仍是本地 CLI 特性,不通过 daemon 端点暴露。更新(update)语义保证:

  • 保留扩展 id、manifest 名称、设置(settings)与激活策略不变;
  • "已经是最新"返回成功的updated: false结果("Already current" is a successfulupdated: falseresult);
  • 卸载(uninstall)是幂等的,同时移除工件与策略。

工作区投影面(Workspace projection)

GET /workspaces/:workspace/extensions PUT /workspaces/:workspace/extensions/activation PUT /workspaces/:workspace/extensions/:extensionId/activation DELETE /workspaces/:workspace/extensions/:extensionId/activation POST /workspaces/:workspace/extensions/refresh

设计上它刻意没有工作区工件变更路由——工作区只有激活与刷新的投影能力,工件变更一律走全局面。投影条目包含:默认值(default)、精确工作区值(exact workspace value)、生效值(effective value)与来源(source)。期望 generation(desired generation)与本地已应用 generation(locally applied generation)是响应中的顶层字段,客户端可以据此判断"策略已持久化"与"运行时已应用"之间的差距。

在源码 workspace-extensions.ts 中可以看到/operations/:operationId路由(如GET ${base}/operations/:operationId,base 随全局/工作区面不同而变化)的实现,以及操作交互(interactions)子路由operations/:operationId/interactions/:interactionId,用于安装过程中的交互式确认。

操作语义:202、Location 与 Retry-After

可能较慢的变更返回202,并附带Location(指向操作记录)与Retry-After(建议轮询间隔)。操作记录的特点:

  • 保存在daemon 进程内内存中;
  • 最多保留100 条终端记录
  • daemon 重启后可能消失——因此目录/存储恢复(catalog/store recovery)才是权威数据源;
  • SDK 轮询超时只停止轮询,绝不取消已受理的工作(never cancels accepted work)。

并发上限与两级 FIFO 队列

daemon 同时受理的未完成扩展操作最多 10 个。在这之上,两级 FIFO 队列控制资源:

  • 准备队列(preparation queue):daemon 全局 FIFO,同时最多执行 2 个下载、解压、转换或单扩展更新检查;
  • 提交队列(commit queue):独立的单并发 FIFO,按准备完成的顺序进入。

各类操作进入的队列不同:安装与更新走prepare -> commit/dispose完整生命周期;激活与卸载只进提交队列;check-updates只进准备队列。手动刷新(manual refresh)通过提交队列串行化,且其 HTTP 超时释放该通道(lane),因此一个卡住的运行时刷新不会永久阻塞后续扩展变更;已经开始的那次刷新之后仍可能自行收敛。

敏感设置的原子 bundle

扩展设置中的敏感项(如 API key、令牌)被暂存为每次准备(per-prepare revision)下的一个原子机密 bundle(secret bundle)。stage 工件内部只记录一个非机密选择器(non-secret selector),它指向该修订号与安全存储后端。这样:

  • 只有胜出的工件提交才会激活一个完整 bundle(不会出现"新代码配旧密钥"的混搭);
  • store 提交是持久化点,并立即释放提交通道
  • 后续的扩展重载、legacy 逐键设置同步、manager 运行时刷新、已准备文件清理、daemon 运行时对账都在提交之后异步执行,不占用两个槽位——因此后面的提交可以在前面 generation 仍在应用/清理时继续推进。

Dispose 一个已准备的变更会移除其未被选中的凭据快照;成功提交会尽力移除先前选中的快照。若进程在 dispose 前硬崩溃,安全后端可能残留一条不可达条目,但没有任何工件选择器引用它,所以它既不会激活,也不会被误认为已提交凭据。

超时、取消与下载上限

  • 准备截止时间(preparation deadline)从操作首次获得准备槽位时开始计时,等待槽位的时间不计入;
  • 中止(abort)会传播给网络操作以及进行中的归档扫描与解压流;即使任务忽略中止,已启动任务也会继续占用槽位直到其底层 promise 落定;
  • 提交不可取消
  • 已准备的更新携带目标工件 generation:无关的扩展或激活变更可以安全 rebase,而同一工件的过期更新会以extension_conflict失败。

远程下载限制在源码中有明确对应:

  • npm 元数据流式读取,10 MiB 响应上限NPM_METADATA_MAX_BYTES = 10 * 1024 * 1024,见 npm.ts);
  • npm 与 GitHub 归档分别有100 MiB 下载上限NPM_ARCHIVE_DOWNLOAD_MAX_BYTESARCHIVE_DOWNLOAD_MAX_BYTES,见 npm.ts 与 github.ts);
  • marketplace 元数据同样有 10 MiB 上限(MARKETPLACE_MAX_BODY_BYTES,见 marketplace.ts);
  • 同时还有请求截止时间、重定向上限,以及解压前的归档条目校验(archive-entry validation),防止 zip 炸弹与路径穿越类归档。

运行时协调(Runtime reconciliation)

工件提交触发刷新,激活提交只持久化策略

关键区分在于:

  • 工件提交成功(install/update)会使本地状态失效并刷新受影响的运行时
  • 激活提交只持久化策略;需要立即生效的客户端应另行提交独立的运行时刷新操作(即POST /workspaces/:workspace/extensions/refresh);
  • 全局工件变更会协调本 daemon 内的所有运行时

运行时刷新(reconcile)会刷新:扩展与技能缓存、扩展工具、层级记忆(hierarchical memory)、活动会话的系统指令、可用命令。某个组件失败不会跳过其余组件;会话 RPC 在所有组件尝试完毕后返回合并后的失败结果。

generation 顺序保证与刷新超时

运行时 generation 协调使用 daemon 全局 FIFO,由变更与 generation 轮询器共享。变更在持久化提交回调(durable commit callback)处预留位置,因此即使较早的提交后工作较晚完成,较晚的 generation 也不可能先刷新运行时。配套保证:

  • 应用 generation N 同时满足等待较旧 generation 的等待者;迟到的低 generation 刷新不能把已应用 generation 往回拨
  • ACP bridge 将每次会话刷新限制在30 秒内;若聚合刷新仍超过路由截止时间,控制器释放提交通道但不取消底层 RPC
  • 部分刷新失败或提交后重载/清理失败产生succeeded_with_warnings,附工作区级或提交级诊断,不回滚工件

迁移失败判定与警告分层

Legacy 工作区迁移中,只有"工件无法重载"才把已提交工件判为失败;设置兼容同步、清理或运行时刷新警告不会触发对已持久化安装工件的重试。更新调用方收到的警告分两类:

  • 兼容性/清理警告:updated with warnings状态;
  • 重载或运行时刷新失败:updated, needs restart状态。

文件监视与 30 秒轮询

扩展文件监视器(watcher)对策略只观察extension-store/state.json(策略 generation),同时继续观察已安装/链接扩展的内容变更(命令、技能、agent、hook、MCP 变化)。30 秒 generation 轮询修复遗漏的文件系统事件,并约束其他共享该 store 的 daemon 的收敛时间——这是多进程共享QWEN_HOME场景下状态最终一致的关键兜底。

兼容性与客户端调用指南

能力标签检查清单

客户端在调用 V2 扩展管理 API 前,应按顺序检查:

  1. extension_management_v2:V2 全局目录/变更 + 工作区限定激活投影存在。必须显式检查——daemon 模式或其他 workspace 能力都不代表该 API 存在;
  2. extension_batch_activation_v2:批量激活(PUT /extensions/activationPUT /workspaces/:workspace/extensions/activation)可用;较老 V2 daemon 只有单数激活路由(PUT .../:extensionId/activation);
  3. extension_activation_explicit_refresh:决定激活后是否需要显式刷新。该标签存在时,激活操作只提交策略,客户端应等激活操作提交后再提交独立的 refresh 操作;标签不存在时,较老 daemon 在激活操作内部已包含刷新。

与 legacyworkspace_extensions的适配

workspace_extensions仍是既有单数面的能力标签,其处理器调用同一套 manager/coordinator并适配响应:

  • 投影激活(project activation)变为主工作区覆盖(primary workspace override)
  • 用户激活(user activation)保留 legacy 的规则清除行为(rule-clearing);
  • extension_activation_explicit_refresh被通告时,通过任一表面的激活都是仅提交(commit-only)
  • legacy 操作端点把 V2 的 warning 完成状态映射回已发布的 legacy 刷新错误状态

非目标(Non-goals)

设计文档明确列出了 V2 不做的事:

  • 每工作区工件拷贝(per-workspace artifact copies)——资源模型上就否定了多份副本;
  • daemon 注册表或远程确认协议(registry / remote ack);
  • 用户取消已受理操作;
  • 旧二进制与 V2-aware 写入者并发写入同一个QWEN_HOME
  • 在未来的 protocol-v2 迁移之前移除 V1 适配器。

这些非目标界定了 V2 的边界:它解决的是多工作区、多进程下的原子性与一致性,而不是引入中心化注册表或分布式协调。

实现参考索引

想深入阅读源码的读者,建议按以下路径展开:

  • 设计文档:docs/design/extension-management-v2.md
  • 核心存储实现(1783 行,含锁、generation、迁移、恢复):packages/core/src/extension/extension-store.ts
  • 能力标签声明(extension_management_v2extension_batch_activation_v2extension_activation_explicit_refreshworkspace_extensions):packages/cli/src/serve/capabilities.ts
  • Daemon 路由实现(含/operations/:operationId与交互子路由):packages/cli/src/serve/routes/workspace-extensions.ts
  • 运行时刷新协调:packages/core/src/extension/extension-runtime-refresh.ts
  • 下载上限常量(npm 10 MiB/100 MiB、GitHub 100 MiB、marketplace 10 MiB):npm.ts、github.ts、marketplace.ts
  • 跨进程串行化与 V1 迁移测试:packages/core/src/extension/extension-store.test.ts
  • 扩展 Git 凭据(配合敏感设置 bundle 阅读):packages/core/src/extension/extension-git-credentials.ts

综上,Extension Management V2 的设计核心是把"工件"与"策略"彻底解耦:工件只有一个、存放在QWEN_HOME/extensions,所有变更经ExtensionStore以 prepare/commit 事务原子落盘;激活策略则按"精确覆盖 → inherit 掩码 → V1 规则 → 全局默认"分层解析,并通过能力标签把提交、刷新、批量激活的语义变化显式暴露给客户端。理解这一模型,是在多工作区、多进程场景下正确使用 qwen-code 扩展体系的前提。

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

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

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

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

立即咨询