Vscode Extension 右键多级子菜单:从 package.json 到 submenu 的完整配置指南

发布时间:2026/10/2 6:18:50
Vscode Extension 右键多级子菜单:从 package.json 到 submenu 的完整配置指南 1. 右键菜单为什么要做多级子菜单VSCode 扩展开发里contributes.menus是最容易被低估的一块。很多人第一次写右键菜单就是在explorer/context里塞两三个command跑起来能用但命令一多就崩了——右键资源管理器菜单拉出来一长条用户根本找不到想要的那一项。我见过一个内部工具扩展光右键菜单就挂了二十多个命令从「初始化仓库」到「清理缓存」全平铺结果同事用了一周都没发现「同步清单」藏在最底下。多级子菜单解决的就是这个问题。它把相关命令收进一个父级菜单项鼠标悬停后向右展开下一层层级清晰、点击路径短。VSCode 官方把这套机制叫submenu配置入口全部在package.json的contributes节里不需要写额外的注册代码纯声明式。具体来说一个多级右键菜单由三部分拼起来contributes.submenus声明子菜单的id和显示用的label相当于给每个层级起个名字。contributes.menus把子菜单挂到某个菜单容器上比如explorer/context资源管理器右键、editor/context编辑器右键。挂载时用submenu: 你的id而不是command。contributes.commands叶子节点最终还是要落到具体命令上每个命令必须有command和title。这套结构支持任意层级嵌套一级菜单挂子菜单 A子菜单 A 里再挂子菜单 BB 里放命令。理论上你想套几层都行但实际产品里超过三层体验就开始变差建议控制在两层到三层。适合谁看这篇如果你正在写 VSCode 扩展需要给右键菜单做分组或者你已经写了平铺菜单现在想改成层级结构再或者你照着文档配了submenu但菜单死活不展开——这篇会从package.json逐字段讲到开发宿主里的验证步骤配置片段可以直接复制改。需要说明的是本文聚焦菜单声明本身。命令的具体实现registerCommand回调里干什么不在范围内但我会在验证环节给出最小可跑的extension.ts骨架保证你能看到菜单真的展开。2. 动手前的环境与 TaoToken 配置准备写扩展之前先把两件事理清楚一是本地开发环境二是如果你打算在扩展里调用大模型能力比如右键菜单触发代码解释、清单生成需要提前把 API 接入配好。后者用 TaoToken 来做会省很多事它的接口兼容主流协议Base URL 和 Key 配一次就能在扩展里复用。先说本地环境。你需要Node.js 18 或更高版本node -v能打印版本号即可。VSCode 稳定版建议 1.80 以上submenu相关字段在老版本里行为有差异。一个扩展脚手架。最省事的是用 Yeomannpm install -g yo generator-code然后yo code选 TypeScript 新建。也可以手动建目录但脚手架会帮你把package.json、tsconfig.json、.vscode/launch.json都生成好调试配置不用自己写。脚手架生成后目录结构大致是这样my-extension/ ├── package.json ├── tsconfig.json ├── src/ │ └── extension.ts └── .vscode/ └── launch.jsonpackage.json里的contributes就是本文的主战场。src/extension.ts负责注册命令回调。.vscode/launch.json里有一个「Run Extension」配置按 F5 会打开一个「扩展开发宿主」窗口你在这个新窗口里右键才能看到自己声明的菜单。再说 TaoToken 的接入。如果你的右键菜单命令要调模型比如「解释选中代码」「生成提交信息」那扩展运行时会发 HTTP 请求。这时候需要三样东西Base URL、API Key、Model ID。Base URL 填https://taotoken.net/api注意这个地址不带任何查询参数。API Key 在控制台创建地址是https://taotoken.net/console/api-keys创建后复制保存页面上只显示一次。Model ID 按你实际要用的模型填比如对话类、代码类各有对应标识在模型列表里能查到。如果你只是先验证菜单结构不调模型那这一步可以跳过等菜单跑通了再回来配。但我的建议是提前把 Key 建好因为扩展里读配置通常走vscode.workspace.getConfiguration你可以在settings.json里预留字段后面接模型时不用改结构。配置读取的典型写法是在扩展的settings里加{ myExtension.apiBaseUrl: https://taotoken.net/api, myExtension.apiKey: , myExtension.modelId: your-model-id }然后在extension.ts里用vscode.workspace.getConfiguration(myExtension).getstring(apiKey)取出来。这样 Key 不进代码仓库用户也能自己覆盖。有一点要提醒扩展开发宿主里的settings.json和你日常用的 VSCode 是分开的调试时要在宿主窗口里单独配别在主窗口配完发现读不到。环境准备好之后下一节直接进package.json的完整配置。我会给一份能直接复制、包含三级菜单的片段你改改 id 和 label 就能用。3. package.json 完整配置submenu 三级嵌套写法这一节是核心。我把一份包含一级入口、二级分组、三级命令的package.json配置拆开讲每个字段都标清楚作用。你可以整段复制到自己的contributes里再把id、label、command换成自己的。先看整体结构。contributes下需要四个平级字段commands、submenus、menus。其中menus里既挂一级入口也挂各级子菜单的内容。{ contributes: { commands: [ { command: edkrepo.menus.manifest, title: edkrepo: 查看清单 }, { command: edkrepo.menus.clone, title: edkrepo: 克隆 }, { command: edkrepo.menus.status, title: edkrepo: 状态 }, { command: edkrepo.menus.clean, title: edkrepo: 清理 }, { command: edkrepo.clone.help, title: edkrepo clone: 帮助 }, { command: edkrepo.clone.verbose, title: edkrepo clone: 详细输出 } ], submenus: [ { id: edkrepoTool, label: edkrepo }, { id: edkrepoClone, label: clone 选项 } ], menus: { explorer/context: [ { submenu: edkrepoTool, group: navigation10, when: explorerResourceIsFolder } ], edkrepoTool: [ { command: edkrepo.menus.manifest, group: 1_manifest1 }, { command: edkrepo.menus.status, group: 2_repo1 }, { command: edkrepo.menus.clean, group: 2_repo2 }, { submenu: edkrepoClone, group: 3_clone1 } ], edkrepoClone: [ { command: edkrepo.clone.help, group: 1_help1 }, { command: edkrepo.clone.verbose, group: 1_help2 } ] } } }逐块解释。commands是叶子命令的登记处。每个命令必须有唯一的command字符串和title。title是显示在菜单里的文字可以带冒号做前缀分组。注意commands里登记的命令必须在extension.ts里用vscode.commands.registerCommand注册否则点击会报「command not found」。submenus声明子菜单的id和label。id是内部引用用的label是鼠标悬停时显示的父级文字。这里我定义了两个edkrepoTool作为一级入口edkrepoClone作为二级里的嵌套项。menus是挂载点。explorer/context表示资源管理器右键菜单这里用submenu: edkrepoTool把一级子菜单挂上去。when条件explorerResourceIsFolder表示只在右键文件夹时出现右键文件时不显示。group里的navigation10控制排序navigation是内置分组10是组内序号数字越小越靠前。edkrepoTool这个 key 对应子菜单 id里面的每一项要么是command要么是submenu。前三条是命令第四条submenu: edkrepoClone把二级子菜单嵌进来形成三级结构。group用1_manifest、2_repo、3_clone这种带前缀的写法VSCode 会按字母序排列分组组之间自动加分隔线。edkrepoClone这个 key 对应二级子菜单 id里面放最终命令。几个容易踩的点第一submenus里的id和menus里的 key 必须完全一致大小写敏感。写成edkrepoTool和edkrepotool是两个东西菜单不会展开。第二menus里挂子菜单用submenu字段挂命令用command字段不能混。有人写成command: edkrepoTool结果 VSCode 去找同名命令找不到就静默失败。第三when条件要写对。explorerResourceIsFolder是资源管理器场景下的上下文键如果你在editor/context里用这个条件菜单永远不显示。编辑器右键要用editorHasSelection之类的键。第四group排序。同一层级里group字符串按字典序排后面的数字是组内序号。想让某项排最前用navigation1想单独成组加分隔线换个前缀即可。这份配置跑起来后右键文件夹会看到「edkrepo」一项悬停展开二级里面有「查看清单」「状态」「清理」和「clone 选项」再悬停「clone 选项」展开三级出现「帮助」和「详细输出」。三层结构点击路径清晰。如果你要加第四层照这个模式再来一轮在submenus里加新 id在上一级菜单里用submenu引用在新 id 对应的menuskey 里放命令。层级没有硬性上限但每多一层用户就多一次悬停实际用起来三层已经够复杂了。4. 在扩展开发宿主中验证多级菜单展开配置写完得亲眼看到菜单展开才算数。这一节给出最小可跑的extension.ts以及从 F5 到右键验证的完整步骤。先补extension.ts。菜单里的每个command都要有对应注册否则点击报错。最小骨架import * as vscode from vscode; export function activate(context: vscode.ExtensionContext) { const commands [ edkrepo.menus.manifest, edkrepo.menus.clone, edkrepo.menus.status, edkrepo.menus.clean, edkrepo.clone.help, edkrepo.clone.verbose ]; commands.forEach((cmd) { const disposable vscode.commands.registerCommand(cmd, () { vscode.window.showInformationMessage(执行命令: ${cmd}); }); context.subscriptions.push(disposable); }); } export function deactivate() {}这段代码把六个命令全部注册点击后弹一个信息提示方便确认命令真的被触发。实际项目里你会在这里调业务逻辑比如读文件、发请求。接下来验证步骤第一步确认package.json保存无误。VSCode 对contributes有 schema 校验字段拼错会在package.json里标黄线。把鼠标悬停在黄线上能看到具体错误比如「Property submenu is not allowed」。先解决所有校验错误再往下走。第二步按 F5 启动扩展开发宿主。如果.vscode/launch.json是脚手架生成的直接 F5 就行。宿主窗口标题栏会显示[Extension Development Host]这是独立于你主窗口的实例。第三步在宿主窗口里打开一个包含文件夹的工作区。随便建个空目录用「打开文件夹」加载进去因为我们的when条件是explorerResourceIsFolder必须有文件夹才能触发。第四步在资源管理器里右键那个文件夹。菜单应该出现「edkrepo」一项。鼠标悬停二级菜单向右展开看到「查看清单」「状态」「清理」「clone 选项」。再悬停「clone 选项」三级菜单展开出现「帮助」「详细输出」。第五步点击任意一项底部弹出「执行命令: xxx」提示说明命令注册成功。如果菜单没出现按这个顺序排查先看when条件。右键的是文件而不是文件夹换成文件夹再试。工作区没加载先打开文件夹。再看submenus的id和menus的 key 是否一致。这是最高频的错误肉眼比对一遍。然后看menus里挂载的容器对不对。explorer/context是资源管理器editor/context是编辑器挂错容器菜单不会出现在预期位置。最后看扩展是否真的激活。有些扩展用activationEvents控制激活时机如果没配onCommand或*扩展可能没启动命令自然没注册。脚手架默认是activationEvents: []配合main入口VSCode 1.74 之后命令会自动触发激活一般不用手动配。但如果你改过确认一下。调试时有个技巧在宿主窗口按CtrlShiftP输入「Developer: Reload Window」可以重载改完package.json后不用关掉宿主重开重载就能生效。改extension.ts则需要重新 F5因为编译产物要更新。还有一点宿主窗口的扩展是临时加载的关掉就没了不会污染你日常用的 VSCode。每次 F5 都是干净环境这点对反复调试很友好。5. 常见报错排查401、local proxy failed 与菜单不展开菜单结构跑通之后如果你的命令里要调模型就会遇到网络和鉴权类报错。这一节把几类高频错误对照着讲包括菜单本身的问题和 API 调用的问题。先说菜单不展开。前面提过 id 不一致这里补充几个更隐蔽的。报错表现右键能看到一级菜单项但悬停不展开或者展开是空的。原因一submenus里声明了 id但menus里没有对应的 key。比如你写了{ id: edkrepoTool, label: edkrepo }但menus里只有explorer/context没有edkrepoTool这个 key。VSCode 找不到内容展开就是空的。解决确保每个submenus的 id 在menus里都有同名 key。原因二menus里的 key 拼写和 id 不一致。大小写、连字符、下划线都要完全一致。原因三when条件在子菜单内容里把项全过滤掉了。比如edkrepoTool里每一项都写了when: explorerResourceIsFolder但你右键的是文件全部不满足展开为空。解决子菜单内容里的when要么去掉要么和父级条件保持一致。再说 API 调用类报错。假设你的命令里用fetch或axios请求 TaoToken 接口。报错401 Unauthorized。这是 Key 的问题。检查三处Key 是否复制完整控制台只显示一次没存就得重建请求头是否带了Authorization: Bearer 你的KeyBase URL 是否写成了https://taotoken.net/api而不是带其他路径。401 基本都是 Key 缺失或错误和菜单无关。报错local proxy failed或连接被拒绝。这类通常是本地网络配置问题。检查你的请求地址是否可达curl https://taotoken.net/api看能否返回。如果公司网络有出口限制需要在扩展里走系统代理设置或者确认当前环境允许访问该域名。注意不要在代码里硬编码任何代理地址用系统默认即可。报错Cannot read properties of undefined (reading choices)。这是响应解析问题。模型接口返回结构里choices是数组如果你拿到的响应是错误对象比如 401 的 body去取choices[0]就会报这个。解决先判断response.ok再解析 JSON错误分支单独处理。const res await fetch(${baseUrl}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${apiKey} }, body: JSON.stringify({ model: modelId, messages: [...] }) }); if (!res.ok) { const errText await res.text(); vscode.window.showErrorMessage(请求失败 ${res.status}: ${errText}); return; } const data await res.json(); const content data.choices?.[0]?.message?.content ?? ;这段用可选链兜底即使结构异常也不会崩。报错OAuth相关。如果你用的是需要 OAuth 的模型服务扩展里做设备码流程比较麻烦。TaoToken 走 API Key 方式不涉及 OAuth配好 Key 直接请求即可。如果你在别处看到 OAuth 报错确认是不是混用了两套鉴权方式。还有一类是命令点击无反应。菜单显示了点击没弹提示。检查extension.ts里是否注册了该命令命令字符串是否和package.json里完全一致。注册时少一个点、多一个空格都会导致找不到。排查顺序建议先确认菜单结构看得到、展得开再确认命令注册点了有反应最后确认 API 调用请求通、解析对。一层一层来别混在一起查。6. 把菜单接上模型能力下一步怎么做菜单骨架跑通、命令能触发之后接下来就是让命令真正干活。如果你的场景是右键文件夹做代码相关操作比如生成清单、解释目录结构、批量重命名那命令回调里调模型是最自然的延伸。接入路径很直接在extension.ts的命令回调里读配置、拼请求、处理响应。配置读取用vscode.workspace.getConfiguration请求发到https://taotoken.net/apiKey 从设置里取。这样用户装完扩展在设置里填自己的 Key 就能用你不用把 Key 打进包里。如果你还没建 Key去https://taotoken.net/console/api-keys创建一个复制保存。想先试试模型返回效果可以在https://taotoken.net/models的对话页面试几条 prompt确认模型行为符合预期再写进扩展。对于需要长期跑编码任务、Agent 类场景的可以看下 Coding Plan地址是https://taotoken.net/coding-plan适合把模型能力持续接进开发流程。接入文档在https://taotoken.net/doc里面有各语言的请求示例照着改请求体就行。回到菜单本身几个实用建议收尾。group的排序规则值得花五分钟调一下。同一层级里group字符串按字典序排用1_、2_、3_前缀能精确控制顺序组之间自动加分隔线。别用a、b、c可读性差后面加项容易乱。when条件可以组合。比如when: explorerResourceIsFolder resourceExtname .git只在右键.git文件夹时显示。上下文键在 VSCode 文档的when子句参考里有完整列表按需查。子菜单的label别太长。右键菜单宽度有限超过十来个字会被截断。用简短动词或名词比如「仓库操作」「清单工具」比「edkrepo 仓库管理相关操作」清爽得多。最后多级菜单不是越深越好。两层能解决的就别做三层三层能解决的就别做四层。用户右键是为了快速执行每多一次悬停都是成本。把高频命令放浅层低频的收进深层这个平衡比技术实现更影响体验。配置片段和验证步骤都在上面了复制改改就能用。遇到菜单不展开先查 id 一致性遇到请求报错先查 Key 和 Base URL。这两条能解决八成问题。