
1. 为什么我要自己写一个 VSCode 插件你可能每天都在用 VSCode装过 Prettier、ESLint、GitLens 这些插件但有没有想过这些插件到底是怎么跑起来的我最早接触插件开发是因为团队里有个重复劳动——每次新建组件文件都要手动敲一遍模板代码、写注释头、补 import。后来我干脆写了个小插件按一个快捷键就自动生成文件骨架省下来的时间够我多喝两杯咖啡。VSCode 插件本质上就是一个 Node.js 包通过官方提供的 API 和编辑器通信。它能做的事情比你想的多注册命令、监听文件变化、自定义代码补全、加侧边栏面板、改主题配色甚至接入大模型做代码问答。对于有前端基础的开发者来说门槛其实不高——你会写 TypeScript、懂一点 npm 包管理就能上手。这篇文章面向的是想从零走通「初始化 → 写逻辑 → 本地调试 → 打包发布」全流程的开发者。我会把每一步的命令、配置文件、验证方式都写清楚你跟着敲一遍就能得到一个能跑、能装、能分享的插件。过程中如果涉及调用大模型能力我会用 TaoToken 的 API 做示例因为它兼容 OpenAI 格式接入成本低适合在插件里做 AI 辅助功能。先说清楚适合谁有 JavaScript/TypeScript 基础用过 VSCode 至少半年知道package.json是干嘛的。如果你完全没写过 Node 项目建议先补一下 npm 的基本操作。下面正式开始。2. 环境准备与 yo code 脚手架初始化2.1 安装必备工具在动手之前确认你机器上有 Node.js 和 npm。打开终端执行node -v npm -vNode 版本建议 18 以上VSCode 插件开发对 Node 版本有要求太老的版本会在调试时报错。如果版本不够去 Node 官网下载 LTS 版本装上。接着全局安装 Yeoman 和 VSCode 官方脚手架生成器npm install -g yo generator-code这两个包的作用是yo是脚手架运行器generator-code是 VSCode 官方维护的模板集合。装完之后你就有了一套标准化的插件初始化流程。2.2 用 yo code 生成项目骨架在你想放项目的目录下执行yo code这时候会出现交互式菜单让你选择插件类型。常见的几个选项选项含义适用场景New Extension (TypeScript)TypeScript 新插件推荐类型提示友好New Extension (JavaScript)JavaScript 新插件不想配 TS 时用New Color Theme新配色主题做主题插件New Language Support新语言支持做语法高亮New Code Snippets新代码片段做 snippet 集合我们选New Extension (TypeScript)。接下来它会依次问你插件名称比如my-first-extension标识符identifier默认和名称一致小写描述一句话说明插件干嘛的是否初始化 Git 仓库选 Yes用哪个包管理器npm / yarn / pnpm 都行是否用 webpack 打包新手选 No简单直接回车之后脚手架会自动生成目录结构并安装依赖。等它跑完你会看到这样的结构my-first-extension/ ├── .vscode/ │ ├── launch.json │ └── tasks.json ├── src/ │ └── extension.ts ├── package.json ├── tsconfig.json └── README.md其中src/extension.ts是入口文件.vscode/launch.json是调试配置package.json是插件的「身份证」后面会重点讲。2.3 第一次跑起来进入项目目录用 VSCode 打开cd my-first-extension code .在 VSCode 里按F5会弹出一个新的「扩展开发宿主」窗口。这个窗口里加载了你正在开发的插件。按CtrlShiftP打开命令面板输入Hello World应该能看到你插件注册的命令。执行它右下角会弹出Hello World from my-first-extension!的提示。到这一步说明脚手架、编译、调试链路全部通了。接下来我们深入配置文件搞清楚每个字段的作用。3. package.json 关键字段与调试配置详解3.1 package.json 里的插件声明package.json是 VSCode 识别插件的核心文件。除了常规的name、version、dependencies还有几个字段专门给 VSCode 用{ name: my-first-extension, displayName: My First Extension, description: 一个演示用的 VSCode 插件, version: 0.0.1, engines: { vscode: ^1.85.0 }, categories: [Other], activationEvents: [], main: ./out/extension.js, contributes: { commands: [ { command: my-first-extension.helloWorld, title: Hello World } ] }, scripts: { vscode:prepublish: npm run compile, compile: tsc -p ./, watch: tsc -watch -p ./ }, devDependencies: { types/vscode: ^1.85.0, types/node: ^18.x, typescript: ^5.x } }几个关键点engines.vscode声明你的插件兼容的最低 VSCode 版本。写太高老版本用户装不了写太低可能用不了新 API。一般写你开发时用的版本就行。activationEvents是激活事件决定插件什么时候被加载。早期版本必须显式声明比如onCommand:xxx。从 VSCode 1.74 开始如果contributes.commands里注册了命令对应的onCommand会自动生成所以这里可以留空数组。但如果你要监听文件打开、启动时激活还是得手动写比如onLanguage:typescript、*启动即激活慎用影响性能。main指向编译后的入口文件。TypeScript 项目编译后输出到out/目录所以写./out/extension.js。contributes.commands是命令贡献点每个命令有command唯一 ID和title命令面板显示的名字。这个 ID 要和代码里registerCommand的第一个参数完全一致否则命令找不到。3.2 调试配置 launch.json脚手架生成的.vscode/launch.json长这样{ version: 0.2.0, configurations: [ { name: Run Extension, type: extensionHost, request: launch, args: [--extensionDevelopmentPath${workspaceFolder}], outFiles: [${workspaceFolder}/out/**/*.js], preLaunchTask: ${defaultBuildTask} } ] }type: extensionHost是 VSCode 插件调试专用的类型。args里的--extensionDevelopmentPath告诉 VSCode 从哪个目录加载插件。outFiles指定编译产物路径方便断点映射。preLaunchTask会在调试前自动执行编译任务对应.vscode/tasks.json里的npm: watch或npm: compile。如果你改了main的输出路径记得同步改outFiles否则断点打不上。3.3 接入 AI 能力时的配置片段很多插件会加 AI 辅助功能比如选中代码让模型解释、生成注释。这时候你需要一个兼容 OpenAI 格式的 API。我用 TaoToken 做示例它的 Base URL 是https://taotoken.net/api在插件里可以这样配置一个 settings 项让用户自己填 Key{ contributes: { configuration: { title: My First Extension, properties: { myFirstExtension.apiKey: { type: string, default: , description: TaoToken API Key用于 AI 辅助功能 }, myFirstExtension.baseUrl: { type: string, default: https://taotoken.net/api, description: API Base URL }, myFirstExtension.modelId: { type: string, default: gpt-4o-mini, description: 模型 ID } } } } }这样用户在 VSCode 设置里就能填 Base URL、Key、Model ID 三件套。代码里通过vscode.workspace.getConfiguration(myFirstExtension)读取。注意不要把 Key 硬编码在源码里也不要把带 Key 的配置提交到 Git。4. 注册命令与验证请求的完整步骤4.1 写一个带参数的命令打开src/extension.ts把默认的helloWorld改造成能接收用户输入、并调用 API 的命令import * as vscode from vscode; export function activate(context: vscode.ExtensionContext) { const disposable vscode.commands.registerCommand( my-first-extension.helloWorld, async () { const editor vscode.window.activeTextEditor; if (!editor) { vscode.window.showWarningMessage(请先打开一个文件); return; } const selection editor.document.getText(editor.selection); if (!selection) { vscode.window.showWarningMessage(请先选中一段代码); return; } const config vscode.workspace.getConfiguration(myFirstExtension); const apiKey config.getstring(apiKey); const baseUrl config.getstring(baseUrl); const modelId config.getstring(modelId); if (!apiKey) { vscode.window.showErrorMessage(请先在设置里配置 API Key); return; } await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, title: 正在请求模型... }, async () { try { const res await fetch(${baseUrl}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${apiKey} }, body: JSON.stringify({ model: modelId, messages: [ { role: system, content: 你是一个代码解释助手用中文简洁说明。 }, { role: user, content: 解释这段代码\n${selection} } ] }) }); const data await res.json(); const reply data.choices?.[0]?.message?.content ?? 无返回内容; const doc await vscode.workspace.openTextDocument({ content: reply, language: markdown }); await vscode.window.showTextDocument(doc, { viewColumn: vscode.ViewColumn.Beside }); } catch (err) { vscode.window.showErrorMessage(请求失败${(err as Error).message}); } } ); } ); context.subscriptions.push(disposable); } export function deactivate() {}这段代码做了几件事检查有没有打开文件、有没有选中内容从配置读三件套用fetch发请求把返回内容开在一个新标签页里。withProgress让用户看到进度提示体验更好。4.2 验证命令是否注册成功按F5启动调试窗口在新窗口里按CtrlShiftP输入Hello World。如果能看到命令说明package.json的contributes.commands和代码里的registerCommand对上了。选中一段代码执行命令。如果配置了正确的 Key几秒后旁边会打开一个新文档里面是模型返回的解释。如果没配 Key会弹出错误提示——这也是验证配置读取逻辑是否生效的方式。4.3 验证激活事件想确认插件是不是在预期时机被激活可以在activate函数第一行加一句console.log(插件已激活时间, new Date().toISOString());然后在调试窗口里按CtrlShiftI打开开发者工具看 Console 面板。如果你在activationEvents里写了onLanguage:typescript那么打开一个.ts文件时应该能看到这条日志。如果没看到检查activationEvents拼写或者命令是否真的被触发。5. 常见报错排查401、local proxy failed 与 reading choices5.1 401 Unauthorized这是最常见的错误说明请求带上了 Key但服务端不认。排查顺序先确认 Key 有没有多余空格。从设置里复制出来前后可能带了换行或空格fetch不会自动 trim。可以在代码里加.trim()。再确认 Base URL 拼对了。TaoToken 的 API 地址是https://taotoken.net/api请求路径是/v1/chat/completions拼起来就是https://taotoken.net/api/v1/chat/completions。如果你在 Base URL 末尾多写了/v1就会变成/v1/v1/...直接 404 或 401。最后确认 Key 本身有效。可以去控制台重新生成一个粘贴到设置里再试。5.2 local proxy failed这个报错通常出现在你本地网络环境有代理设置但fetch没走对通道。VSCode 插件运行在扩展宿主进程里它继承的是 VSCode 的网络配置。如果你在 VSCode 设置里配了http.proxy但代理本身不可用就会报这个。解决办法先检查 VSCode 的http.proxy设置如果不需要代理就清空。如果确实需要确认代理地址和端口正确。另外Node 18 自带的fetch对代理的支持不如axios完善如果代理环境复杂可以换成axios并手动配proxy参数。5.3 Cannot read properties of undefined (reading choices)这个错误说明data.choices是 undefined也就是返回的 JSON 结构和你预期的不一样。常见原因请求根本没成功返回的是错误对象比如{ error: { message: ... } }。这时候data.choices自然是 undefined。你可以在解析前先判断res.ok不 ok 就把data.error.message打出来。模型 ID 写错了服务端返回了错误信息而不是正常补全结果。检查modelId配置确认这个模型在你的账号下可用。返回内容被截断或格式异常。加一层防御if (!res.ok) { const errText await res.text(); throw new Error(HTTP ${res.status}: ${errText}); } const data await res.json(); const reply data?.choices?.[0]?.message?.content; if (!reply) { throw new Error(返回结构异常 JSON.stringify(data)); }这样报错信息会清晰很多不用靠猜。5.4 命令找不到或重复注册如果命令面板里搜不到你的命令先看package.json的contributes.commands里command字段和代码里registerCommand的参数是否完全一致包括大小写和点号。VSCode 对命令 ID 是大小写敏感的。如果提示「命令已存在」说明你在activate里重复注册了同一个 ID。检查是不是热重载时旧代码没清理或者context.subscriptions.push漏了导致重复执行。每次注册都应该把返回的 disposable 推进 subscriptionsVSCode 会在插件停用时自动清理。6. 打包发布与后续迭代建议6.1 用 vsce 打包开发完成后用官方工具vsce打包成.vsix文件npm install -g vscode/vsce vsce package它会读取package.json里的信息生成my-first-extension-0.0.1.vsix。这个文件可以直接发给别人对方在 VSCode 里「从 VSIX 安装」就能用。打包前记得补全README.md、LICENSE、CHANGELOG.mdvsce会检查这些文件是否存在。repository字段也要填否则会警告。6.2 发布到市场想发布到 VSCode Marketplace需要先注册一个 Azure DevOps 组织创建 Personal Access Token然后用vsce publish上传。流程稍微繁琐但官方文档写得很清楚。如果你只是内部团队用直接分发.vsix更省事。6.3 迭代时注意的点插件体积要控制。如果你引入了很大的依赖打包出来的 vsix 可能几十 MB用户安装体验差。用 webpack 或 esbuild 做 tree-shaking 能显著减小体积。激活事件要精准。不要图省事写*那会让插件在 VSCode 启动时就加载拖慢启动速度。按需激活比如onCommand、onLanguage。API Key 这类敏感信息永远不要写进代码或提交到仓库。用contributes.configuration让用户自己填或者用context.secrets做加密存储。如果你打算在插件里做更复杂的 AI 功能比如多轮对话、代码补全、Agent 式操作可以了解下 TaoToken 的 Coding Plan它针对长期编码场景做了优化接入方式和普通 API 一致但配额和稳定性更适合高频调用。模型对话页面也能直接测试模型返回效果方便你在写插件前先验证 prompt 和参数。最后一步把package.json里的version改成0.0.2重新vsce package你就完成了第一次迭代。整个过程走一遍后面再写第二个插件就是复制粘贴改逻辑的事了。