1. 为什么我要给 VSCode 加一个只读开关
你有没有过这种经历:打开一个老项目,只是想翻翻代码找找某个函数的实现,结果手一抖按到了键盘,某个字符就被改了,保存的时候才发现 diff 里多了一行莫名其妙的改动。尤其是用 VSCode 看别人的仓库、看线上配置、看编译产物的时候,这种误触特别烦。
VSCode 本身没有内置的「只读模式」开关,社区里关于这个需求的讨论一直都有,但官方并没有给出一个开箱即用的方案。我翻了不少 issue,发现大家的思路基本集中在两个方向:一是想办法拿到 Monaco Editor 的实例直接设成只读,二是从命令层面拦截输入。前者在插件 API 里基本走不通,后者才是真正可落地的路子。
这篇笔记就聚焦 VSCode 插件开发场景,带你从零做一个 read-only 插件:在状态栏放一个按钮,点一下切换只读状态,同时把 TaoToken 的统一 Key/API 通道接进来,方便后续在插件里调用模型能力做代码解释、注释生成之类的扩展。整篇会给到可复制的package.json命令注册、statusbar 创建与切换逻辑骨架,以及settings.json里的 TaoToken 配置片段和本地验证步骤。
适合谁看:写过一点 TypeScript、想入门 VSCode 插件开发的同学;或者已经有一个内部插件、想给它加个只读开关和统一模型通道的开发者。不需要你之前做过插件,跟着敲一遍就能跑起来。
核心检索词先摆在这:VSCode read-only 插件、extension statusbar 开关、TaoToken 统一 Key。下面按「问题场景 → 前置准备 → 可复制配置 → 验证 → 排障 → 收尾」的顺序走。
2. 前置准备:TaoToken 统一 Key 与插件工程骨架
2.1 为什么插件里要接 TaoToken
插件本身做只读切换不需要任何网络请求,但如果你想让这个插件再往前走一步,比如选中一段代码后让模型解释、或者自动生成注释,就需要一个稳定的模型调用通道。TaoToken 提供的是统一的 Key 和 API 入口,你不用在插件里硬编码某一家厂商的地址和密钥,换模型、换通道都只改配置,插件代码不用动。
对插件开发来说这点很关键:插件是要分发给别人用的,如果把密钥写死在代码里,既不安全也没法维护。走 TaoToken 的统一通道,用户在自己机器的settings.json里填自己的 Key 就行,插件只负责读配置、发请求。
2.2 拿到 Key 和确认接入信息
先去控制台创建一个 API Key,地址是 https://taotoken.net/api-keys ,创建完复制出来,后面填到settings.json里。API 的基础地址是 https://taotoken.net/api ,注意这个地址不带任何查询参数,直接作为 base URL 用。
如果你后面想先验证模型通不通,可以打开模型对话页面 https://taotoken.net/models 手动发一条消息试试;如果打算长期在编码和 Agent 场景里用,可以看下 Coding Plan https://taotoken.net/coding-plan ,接入文档在 https://taotoken.net/doc 。
2.3 初始化插件工程
用官方脚手架起一个 TypeScript 插件项目最省事。先装好 Node.js 和yo、generator-code:
npm install -g yo generator-code yo code交互式选择里选New Extension (TypeScript),名字填vscode-readonly,其余默认。生成完目录结构大致是这样:
vscode-readonly/ ├── package.json ├── tsconfig.json ├── src/ │ └── extension.ts └── .vscode/ └── launch.jsonpackage.json是插件的清单文件,命令、配置项、激活事件都在这里声明;src/extension.ts是入口,activate函数在插件被激活时执行。接下来所有改动都围绕这两个文件。
3. 可复制配置:命令注册、statusbar 与切换逻辑
3.1 package.json 里声明命令和配置项
打开package.json,在contributes字段里加三块内容:命令、配置项、以及激活事件。命令是给状态栏按钮点击时调用的,配置项是让用户在settings.json里控制默认只读状态和 TaoToken 参数。
{ "contributes": { "commands": [ { "command": "readonly.toggle", "title": "ReadOnly: 切换只读模式" } ], "configuration": { "title": "ReadOnly", "properties": { "readonly.defaultOn": { "type": "boolean", "default": false, "description": "插件启动时是否默认开启只读模式" }, "readonly.taotokenApiKey": { "type": "string", "default": "", "description": "TaoToken API Key,用于插件内的模型调用" }, "readonly.taotokenBaseUrl": { "type": "string", "default": "https://taotoken.net/api", "description": "TaoToken API 基础地址" } } } }, "activationEvents": [ "onStartupFinished" ] }这里onStartupFinished让插件在 VSCode 启动完成后自动激活,这样状态栏按钮一打开编辑器就能看到。命令readonly.toggle是唯一对外暴露的动作,状态栏点击和命令面板都会走它。
3.2 创建 statusbar 并绑定切换命令
打开src/extension.ts,先写状态栏的创建和切换逻辑。核心思路是维护一个布尔变量isReadOnly,每次切换时更新它、刷新状态栏文案,同时通过vscode.commands.executeCommand把type命令接管或还原。
import * as vscode from 'vscode'; let statusBarItem: vscode.StatusBarItem; let isReadOnly = false; export function activate(context: vscode.ExtensionContext) { const config = vscode.workspace.getConfiguration('readonly'); isReadOnly = config.get<boolean>('defaultOn', false); statusBarItem = vscode.window.createStatusBarItem( vscode.StatusBarAlignment.Right, 100 ); statusBarItem.command = 'readonly.toggle'; updateStatusBar(); statusBarItem.show(); const toggleCmd = vscode.commands.registerCommand('readonly.toggle', () => { isReadOnly = !isReadOnly; updateStatusBar(); vscode.window.showInformationMessage( isReadOnly ? '已进入只读模式' : '已退出只读模式' ); }); context.subscriptions.push(statusBarItem, toggleCmd); } function updateStatusBar() { statusBarItem.text = isReadOnly ? '$(lock) ReadOnly' : '$(unlock) Editable'; statusBarItem.tooltip = isReadOnly ? '当前只读,点击切换为可编辑' : '当前可编辑,点击切换为只读'; }$(lock)和$(unlock)是 VSCode 内置的图标语法,状态栏会直接渲染成小锁图标,比纯文字直观。StatusBarAlignment.Right把它放在右下角,优先级 100 保证它不会被其他插件挤掉。
3.3 拦截 type 命令实现真正的只读
光有状态栏文案还不够,得让键盘输入真的不生效。VSCode 的编辑器在输入字符时会触发type命令,我们把这个命令覆盖掉,只读时什么都不做,可编辑时把参数透传给默认实现default:type。
const typeCmd = vscode.commands.registerCommand('type', async (args) => { if (isReadOnly) { return; } return vscode.commands.executeCommand('default:type', args); }); context.subscriptions.push(typeCmd);把这段也放进activate里。原理很直接:type是编辑器输入的统一入口,覆盖它等于在输入链路上加了一道闸门。只读时直接return,字符就不会进入文档;可编辑时原样转发给default:type,行为跟原生完全一致。
注意:这个方案拦截的是键盘输入,复制粘贴、格式化、批量替换这些操作走的是别的命令,不在本次范围内。如果你要更严格的只读,可以继续覆盖
paste、editor.action.formatDocument等命令,思路是一样的。
3.4 settings.json 里的 TaoToken 配置片段
插件装好后,在用户或工作区的settings.json里填上 TaoToken 相关配置。这样插件读配置就能拿到 Key 和地址,不用改代码:
{ "readonly.defaultOn": true, "readonly.taotokenApiKey": "sk-你的Key", "readonly.taotokenBaseUrl": "https://taotoken.net/api" }defaultOn设成true的话,VSCode 一启动就是只读状态,适合专门用来看代码的场景。Key 建议放在用户级settings.json里,不要提交到仓库;如果是团队共享的工作区配置,Key 那行留空,让每个人自己填。
4. 验证请求:本地跑起来看结果
4.1 启动调试宿主
在 VSCode 里按F5,会弹出一个新的「扩展开发宿主」窗口,这个窗口里加载的就是你刚写的插件。第一次启动会先编译 TypeScript,等编译完成新窗口出现即可。
新窗口右下角应该能看到状态栏按钮,默认显示$(unlock) Editable或$(lock) ReadOnly,取决于你defaultOn设的是啥。点一下按钮,文案会在两个状态间切换,同时弹出提示。
4.2 验证只读是否真的生效
在新窗口里随便打开一个文件,点状态栏切到ReadOnly,然后敲键盘。你会发现光标不动、字符不出现,文档内容完全没变化。再点一下切回Editable,键盘输入恢复正常。这一步是核心验证点,如果只读时还能输入,说明type命令没拦截成功,回去检查命令注册的时机和isReadOnly的初始值。
4.3 验证 TaoToken 配置读取
在extension.ts里加一段临时日志,确认配置能读到:
const apiKey = config.get<string>('taotokenApiKey', ''); const baseUrl = config.get<string>('taotokenBaseUrl', ''); console.log('TaoToken baseUrl:', baseUrl, 'key length:', apiKey.length);按Ctrl+Shift+I打开调试控制台,能看到输出的 baseUrl 和 key 长度。key 长度不为 0 就说明配置读取正常。如果你要真的发一次请求,可以用fetch打https://taotoken.net/api下的对话接口,带上Authorization: Bearer <key>,返回 200 就说明通道通了。手动验证模型是否可用,直接去 https://taotoken.net/models 发一条消息更快。
5. 本篇常见错排查
5.1 状态栏按钮不显示
最常见的原因是activationEvents没配对。如果你写的是onStartupFinished,插件会在启动后激活;如果写成了onCommand:readonly.toggle,那只有手动执行命令才会激活,状态栏自然不出现。另外检查statusBarItem.show()有没有被调用,以及context.subscriptions.push里有没有把它加进去,否则可能被提前回收。
5.2 只读时还能输入
先确认type命令的注册在activate里执行了,并且isReadOnly在切换时确实变了。有个容易踩的坑:isReadOnly如果声明在函数内部而不是模块顶层,每次切换读到的可能是旧值。把它放在模块作用域,activate和命令回调共享同一个变量。还有一种情况是别的插件也覆盖了type,命令注册有先后顺序,后注册的会覆盖先注册的,可以调整插件加载顺序或改用vscode.commands.registerCommand的返回值做链式处理。
5.3 配置项读不到
vscode.workspace.getConfiguration('readonly')里的参数是配置的 section 名,必须和package.json里configuration.properties的前缀一致。如果你在package.json里写的是readonly.taotokenApiKey,那 section 就是readonly,get 的时候传taotokenApiKey。改完package.json记得重新按F5启动宿主,配置清单的变更不会热更新。
5.4 打包成 vsix 后行为不一致
本地调试用的是源码,打包后走的是编译产物。确认tsconfig.json的outDir和package.json的main指向一致,通常是./out/extension.js。打包命令用vsce package,如果提示缺少repository字段,在package.json里补一个即可。装 vsix 用code --install-extension xxx.vsix。
6. 把只读开关和统一通道用起来
到这里,一个能用的 read-only 插件就成型了:状态栏按钮切换、type命令拦截、TaoToken 配置读取三块都跑通了。我自己的用法是把它设成默认只读,专门用来读线上仓库和第三方库源码,需要改的时候点一下解锁,改完再锁上,误触基本绝迹。
如果你打算继续扩展,几个方向可以试试:把paste和editor.action.formatDocument也纳入拦截,只读会更彻底;把 TaoToken 的调用封装成一个explainSelection命令,选中代码后直接让模型解释,配合只读模式看陌生代码效率很高。长期在编码和 Agent 场景里用的话,Coding Plan 那条通道会更顺,接入文档里有完整的参数说明。
Key 管理和接入细节都在 https://taotoken.net/api-keys 和 https://taotoken.net/doc ,遇到请求报错先看返回的状态码和 message,多数是 Key 没填对或者 base URL 多带了斜杠。插件代码本身不复杂,真正花时间的是把命令拦截的边界想清楚,哪些操作该拦、哪些该放,这个取舍按你自己的使用习惯来定就行。