1. 从右键菜单太乱说起:VSCode 插件多级菜单到底解决什么问题
如果你写过 VSCode 插件,大概率遇到过这个场景:功能越加越多,editor/context右键菜单被塞了七八个命令,用户右键一看全是平铺的「解释代码」「生成注释」「写单测」「代码审查」,找起来费劲,插件也显得不专业。这时候就需要package.json里的submenu把命令收进一个一级菜单,展开后再选具体功能。
这篇要解决的就是两件事:一是用contributes.submenus+contributes.menus把多级菜单声明清楚,二是让这些 AI 命令真正能跑起来——也就是统一走 TaoToken 的 Key/API 通道,不用每个命令各配一套密钥。适合正在开发 VSCode AI 插件、被菜单层级和 Key 管理同时卡住的开发者。
我试过把菜单和 Key 分开调,结果菜单能展开但命令一执行就报鉴权失败,来回折腾半天。后来把两件事放一起联调,反而顺了。下面按「菜单骨架 → Key 通道 → 联调验证 → 排错」的顺序走一遍,配置都能直接复制。
2. 前置准备:TaoToken 统一 Key 与 API 通道
多级菜单只是「壳」,点下去要调模型才是「核」。与其在每个命令里硬编码不同的模型地址和密钥,不如统一走一个 API 通道。TaoToken 在这里扮演的就是统一入口:一个 Key、一个 Base URL,插件里所有 AI 命令都复用它。
你需要先拿到 Key。登录后进控制台,在 API Keys 页面创建一个密钥,复制出来(只显示一次,记得存好)。地址是https://taotoken.net/api,注意这个是不带任何查询参数的纯 API 根地址,插件里拼接/v1/chat/completions这类路径时用它做 base。
模型对话入口可以用来先验证 Key 是否可用,不用写代码就能发一条请求看返回。如果你后面要做长期编码类插件、甚至接 Agent 工作流,可以了解下 Coding Plan,它更适合高频调用场景。接入文档里有完整的请求格式和参数说明,配置前扫一眼能少踩坑。
注意:Key 不要写进
package.json或提交到仓库。插件里读 Key 的正确姿势是走 VSCode 的配置项或 SecretStorage,下面会给 settings.json 片段。
3. 可复制的 package.json 多级菜单骨架
先声明菜单结构。核心是三块:submenus定义菜单容器,menus定义谁挂在谁下面,commands定义点击后执行的命令。下面是一个两级菜单的完整骨架,命令名统一用myai.前缀,你可以整体替换成自己的。
{ "contributes": { "submenus": [ { "id": "myai.mainMenu", "label": "AI 助手" }, { "id": "myai.moreMenu", "label": "更多功能" } ], "menus": { "editor/context": [ { "submenu": "myai.mainMenu", "group": "navigation" } ], "myai.mainMenu": [ { "command": "myai.ask", "group": "navigation" }, { "command": "myai.explain", "group": "navigation" }, { "submenu": "myai.moreMenu", "group": "more" } ], "myai.moreMenu": [ { "command": "myai.comment" }, { "command": "myai.review" }, { "command": "myai.unitTest" } ] }, "commands": [ { "command": "myai.ask", "title": "问 AI 助手" }, { "command": "myai.explain", "title": "解释代码" }, { "command": "myai.comment", "title": "添加注释" }, { "command": "myai.review", "title": "优化代码" }, { "command": "myai.unitTest", "title": "生成单元测试" } ] } }几个容易忽略的点。第一,editor/context里挂的是submenu而不是command,submenu的值必须和submenus里的id完全一致,写错一个字符菜单就不显示。第二,子菜单项的排序默认按title字母序,想固定顺序就加group,但group之间会出现分割横线,视觉上要接受。第三,myai.mainMenu这个 key 既是submenus的 id,也是menus里的一个命名空间,别搞混。
三级菜单就是在myai.moreMenu里再挂一个submenu,指向第三个submenus条目,层级可以一直往下套。但实测超过三级用户体验就差了,建议最多两级。
4. settings.json 与命令注册:把 Key 通道接进插件
菜单声明完,命令得在extension.ts里注册,同时把 Key 从配置读出来。先在package.json的contributes.configuration里声明配置项:
{ "contributes": { "configuration": { "title": "AI 助手", "properties": { "myai.apiKey": { "type": "string", "default": "", "description": "TaoToken API Key" }, "myai.baseUrl": { "type": "string", "default": "https://taotoken.net/api", "description": "API 根地址" }, "myai.model": { "type": "string", "default": "gpt-4o-mini", "description": "默认模型" } } } } }用户在settings.json里填:
{ "myai.apiKey": "sk-你的Key", "myai.baseUrl": "https://taotoken.net/api", "myai.model": "gpt-4o-mini" }然后在extension.ts里注册命令并复用同一个请求函数:
import * as vscode from 'vscode'; async function callAI(prompt: string): Promise<string> { const cfg = vscode.workspace.getConfiguration('myai'); const apiKey = cfg.get<string>('apiKey') || ''; const baseUrl = cfg.get<string>('baseUrl') || 'https://taotoken.net/api'; const model = cfg.get<string>('model') || 'gpt-4o-mini'; const res = await fetch(`${baseUrl}/v1/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model, messages: [{ role: 'user', content: prompt }] }) }); if (!res.ok) { throw new Error(`请求失败: ${res.status} ${await res.text()}`); } const data = await res.json(); return data.choices[0].message.content; } export function activate(context: vscode.ExtensionContext) { const register = (id: string, buildPrompt: (code: string) => string) => { const disposable = vscode.commands.registerCommand(id, async () => { const editor = vscode.window.activeTextEditor; if (!editor) { vscode.window.showWarningMessage('请先打开一个文件'); return; } const code = editor.document.getText(editor.selection); try { const result = await callAI(buildPrompt(code)); vscode.window.showInformationMessage(result.slice(0, 200)); } catch (e) { vscode.window.showErrorMessage(String(e)); } }); context.subscriptions.push(disposable); }; register('myai.ask', (c) => `解释这段代码:\n${c}`); register('myai.explain', (c) => `逐行解释:\n${c}`); register('myai.comment', (c) => `为以下代码添加注释:\n${c}`); register('myai.review', (c) => `审查并优化:\n${c}`); register('myai.unitTest', (c) => `生成单元测试:\n${c}`); }这样所有命令共用callAI,Key 只配一次,换模型也只改一个地方。
5. 验证请求与菜单展开:一次跑通联调
配置写完,按 F5 启动扩展开发宿主窗口,打开一个.ts或.js文件,选中几行代码,右键。你应该能看到「AI 助手」一级菜单,悬停展开后是「问 AI 助手」「解释代码」,再往下「更多功能」还能展开出三个命令。
点「解释代码」,如果 Key 和地址都对,几秒后右下角会弹出模型返回的前 200 字。这一步成功说明菜单绑定和 Key 通道都通了。如果只想先验证 Key 本身,可以打开模型对话页面直接发一条消息,确认返回正常再回来调插件,能快速区分是菜单问题还是鉴权问题。
联调时建议开两个窗口:一个跑扩展宿主,一个看调试控制台。callAI里抛出的错误会打到控制台,401基本是 Key 错,404多半是 baseUrl 拼错,429是频率限制。
6. 本篇常见错排查
菜单不显示,九成是submenu的 id 和submenus里的对不上,或者editor/context里误写成了command。改完package.json一定要重启扩展宿主,热重载有时不生效。
命令点了没反应,先看commands里有没有注册对应的command字段,menus里引用的命令必须在commands数组里存在,否则点击静默失败。
排序乱、想固定顺序,给菜单项加group,但记住group会带分割线。不想有横线就接受字母序,或者用group但把相关项放同一组。
请求报401,检查settings.json里 Key 有没有多余空格,以及Authorization头是不是Bearer加 Key。报404就核对 baseUrl 是不是https://taotoken.net/api,别多写或少写路径。
Key 想更安全,别放settings.json明文,改用context.secrets.store存,命令里await context.secrets.get取。这样即使配置同步到云端也不泄露。
菜单和 Key 都跑通后,如果要做长期编码类插件或接 Agent,可以看下 Coding Plan;日常接入配置和参数细节,接入文档里写得更全,遇到鉴权或路径问题对着 API Keys 页面重新生成一个 Key 再试通常能排除环境干扰。