qwen-code Extension Skill 所有者身份模型:extensionName 与 extensionDisplayName 契约详解
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
导读
qwen-code 通过GET /workspace/skills向 ACP 客户端(IDE 集成、TypeScript SDK、Web Shell 等)暴露工作区中可用的 Skill 列表。对于由 Extension 提供的 Skill,该接口需要同时满足两个诉求:客户端能够稳定地建立 Skill 与 Extension 的归属关系(身份),又能拿到适合人类阅读的友好名称(展示)。本文围绕 extension-skill-owner-identity 设计文档 展开,剖析 qwen-code 如何通过extensionName(规范身份)与extensionDisplayName(本地化展示名)双字段模型解决身份混淆问题,并深入到packages/core的 Skill 管理器、ACP Bridge 与 TypeScript SDK 的类型投影实现,帮助你理解该契约的边界与兼容性约束。
问题背景:display name 为什么不能当身份用
在引入本设计之前,GET /workspace/skills返回的extensionName字段实际存放的是 Extension 的本地化展示名(localized display name),而不是 Extension 清单(manifest)中的规范name。这个做法在两个维度上是有问题的:
- 随 locale 漂移:同一个 Extension 在不同语言环境下会解析出不同的 display name,客户端无法把一个 Skill 稳定地归属到其 Extension;
- 不具备唯一性:展示名是给人看的,两个 Extension 完全可能撞名,客户端拿到重名后无法区分。
从源码看,Extension 的规范身份正是其 manifest 中的name。Skill 管理器的注册与枚举都以extension.name为准,并将两个字段显式分离记录(见 skill-manager.ts):
skills.push({ ...skill, name: qualifySkillName(extension.name, skill.name), authoredName: skill.name, extensionName: extension.name, extensionDisplayName: extension.displayName, priority: hasInvalidPriority ? 0 : (skill.priority as number | undefined), });其中qualifySkillName(extension.name, skill.name)生成形如<extensionName>:<authoredName>的注册表身份,从而保证两个 Extension 即使都发布名为pdf的 Skill,也能在/skills列表中产生两个可区分的名字(见 types.ts)。
契约定义:双字段的职责划分
设计文档将所有权信息收敛为两个可选字段,职责严格分离:
| 字段 | 含义 | 用途 | 是否可作身份 |
|---|---|---|---|
extensionName | Extension manifest 中的规范name | 身份匹配、非活跃态检查、快照去重 | ✅ 唯一权威 |
extensionDisplayName | 可选、按 locale 解析出的展示名 | 展示层友好标签 | ❌ 仅展示 |
契约中的关键约束如下:
- 身份匹配、非活跃态检查、快照去重一律只使用
extensionName; - 展示层统一采用
extensionDisplayName ?? extensionName的回退表达式——有展示名用展示名,没有就回退到规范名; - 两个字段都保持可选(optional),原因有二:非 Extension 来源的 Skill 没有所有者;较老版本的 daemon 不输出
extensionDisplayName; - 状态(status)schema维持在 version 1,因为新字段是纯增量(additive)的,不破坏既有消费者。
在核心类型定义中,extensionName被注释为 “the canonical name of the providing extension”,而extensionDisplayName被明确标注为 “Presentation only; never use this field as an identity”(见 types.ts),与契约一一对应。
数据流:从 Extension 枚举到 ACP/TypeScript SDK 状态类型
整条链路分三步走:
- Skill 管理器记录:
SkillManager在枚举活跃(active)Extensions 时,从每个 extension 的 manifest 同时取出name与displayName,写入extensionName/extensionDisplayName(skill-manager.ts); - ACP 工作区快照合成:当从非活跃(inactive)Extensions合成 Skill 时,应用同样的规则——即
extensionName取规范名、extensionDisplayName取展示名,保证活跃与不活跃两条路径行为一致; - 共享映射器投影:共用的 workspace-skills mapper 将这两个值投影进 ACP Bridge 与 TypeScript SDK 的状态类型中,保证两个 SDK 面看到的字段语义完全一致。
从仓库源码可以确认这一投影确实落地到了客户端侧类型:
- ACP Bridge 状态类型中同时存在
extensionName与extensionDisplayName(见 packages/acp-bridge/src/status.ts); - TypeScript SDK 的 daemon 类型同样投影了这两个字段(见 packages/sdk-typescript/src/daemon/types.ts)。
CLI 与 Web Shell 的展示层则读取展示字段并按extensionDisplayName ?? extensionName回退;而 MCP 与 Agent 的所有权元数据属于独立契约,本次改动不触碰,边界划分清晰。
兼容性策略:旧客户端与新客户端的双赢
设计文档明确了三层兼容性保证:
- 现有第三方客户端继续收到
extensionName,但值从"展示文本"被纠正为"manifest 身份"——这是行为修正而非破坏性变更,字段名不变; - 需要友好标签的客户端可以主动采用
extensionDisplayName,并在其缺失时回退到extensionName; - 新客户端面向老版本 daemon 时,由于
extensionDisplayName缺失,同样以extensionName兜底,天然兼容。
测试用例印证了这一行为:在 skill-manager.test.ts 中,断言同时校验了extensionName('alibabacloud-database-suite')与extensionDisplayName的取值,确保身份字段承载的是规范名而非展示名。
实践建议:客户端接入要点
对于基于 qwen-code daemon 开发 ACP 客户端或集成方的读者,本契约带来的实操结论可以归纳为四点:
- 永远不要用 display 类字段做匹配键:无论是 Skill 与 Extension 的 join、非活跃状态的判定,还是快照去重,一律以
extensionName为准; - 展示名一律走回退表达式:
extensionDisplayName ?? extensionName,保证老 daemon 与无主 Skill(字段缺失)场景下 UI 不出现空标签; - schema 版本无需升级:
extensionDisplayName是增量字段,status schema 保持 version 1,消费方无需因该字段做版本协商; - 区分契约边界:MCP 工具、Agent 的所有权元数据与 Extension Skill 的所有权是两套独立契约,不要混用字段做跨域推断。
总结
extensionName+extensionDisplayName的双字段模型,是 qwen-code 在 "机器可用的身份" 与 "人可读的展示" 之间做的一次干净切分:规范名承载唯一性与稳定性,展示名承载可读性,两者通过??回退优雅衔接,并以纯增量字段保证 schema 与旧客户端双兼容。对于任何消费GET /workspace/skills的客户端,牢记 "身份只看extensionName" 这一原则,即可避免本地化与重名带来的归属错乱。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考