天天 alt-tab 切到浏览器问 AI、复制粘贴代码,烦透了。索性给自己写了个 VSCode 扩展:选中一段代码,右键"问 AI",在侧边 webview 里流式出答案。功能听着简单,真做下来发现扩展环境跟普通网页差太多------webview 是个沙箱,网络请求又得绕到 extension host 那侧,SSE 流式在中间传一道,坑比想象的多。下面是踩平的过程。
整体长这样
三层:右键命令 拿到选中代码 → extension host(Node 环境) 去请求 AI 流式接口 → 一段段 postMessage 给 webview 渲染。为啥不让 webview 直接 fetch?因为 webview 跨域受限、还拿不到密钥,正路是让 host 当中转。
注册命令、拿选中代码
package.json 里声明个 contributes.commands 和右键菜单,激活后:
ini
const vscode = require('vscode');
function activate(context) {
const cmd = vscode.commands.registerCommand('askAI.ask', () => {
const editor = vscode.window.activeTextEditor;
const code = editor.document.getText(editor.selection); // 选中的代码
const panel = vscode.window.createWebviewPanel(
'askAI', 'AI 解释', vscode.ViewColumn.Beside,
{ enableScripts: true } // 必须开,不然 webview 里跑不了 JS
);
panel.webview.html = getHtml();
streamToWebview(code, panel.webview); // 见下
});
context.subscriptions.push(cmd);
}
核心:host 侧拉 SSE,逐段 postMessage 进 webview
webview 自己收不了 SSE,我在 host(Node)里读流,每来一段就 postMessage 推进去:
javascript
async function streamToWebview(code, webview) {
const res = await fetch(AGENT_API, {
method: 'POST',
headers: { Authorization: `Bearer ${getKey()}` }, // 密钥在 host,webview 看不到
body: JSON.stringify({ prompt: `解释这段代码:\n${code}` }),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) { webview.postMessage({ type: 'done' }); break; }
const chunk = decoder.decode(value, { stream: true });
// SSE 每行 data: 解析出 delta,推给 webview
for (const line of chunk.split('\n')) {
if (line.startsWith('data: ')) {
webview.postMessage({ type: 'delta', text: parse(line) });
}
}
}
}
webview 那边 window.addEventListener('message', ...) 收着拼上去就行。
我真栽进去的几个坑
1. webview 里写死的资源路径全 404。 webview 加载本地脚本/样式不能用普通相对路径,得用 webview.asWebviewUri() 转一道,我一开始 <script src="./main.js"> 直接白屏,控制台还是 webview 那套受限的,debug 都费劲。
2. node-fetch 的流在不同 Node 版本表现不一。VSCode 内置的 Node 版本不一定带全局 fetch,我为了流式 reader 折腾了半天,最后锁了个明确的 fetch 实现才稳。
3. 取消逻辑容易漏 。用户嫌答得慢关掉 panel 了,host 那侧的 fetch 还在跑、还在 postMessage 一个已经销毁的 webview,报一堆错。得在 panel.onDidDispose 里 abort 掉请求。
4. 密钥别硬编码进扩展 。扩展是要打包分发的,key 写死等于公开。我用 VSCode 的 SecretStorage 存,首次用让用户自己填。
老实讲这扩展我只够自己用,远没到能上市场的程度------错误提示糙、长代码会超上下文我也没做截断。但"选中即问、不用切窗口"这一下,确实顺手太多了。
后端那个 AGENT_API 不是我写的,是拿一个零代码搭智能体的平台配的:塞了点我们项目的编码规范当知识库、发布成 API,所以它解释代码时还能带上我们团队的约定,比通用模型贴合。
你们有给 IDE 写过自用小扩展吗?最想让 AI 在编辑器里帮你干啥?评论区聊聊。
(后端模型走的讯飞 MaaS,现成 API,扩展 host 侧直接调,密钥锁在 SecretStorage)