1. 为什么要在 VSCode 里做「前端每日一题」插件
每天刷一道前端面试题,这件事听起来简单,真正坚持下来的人却不多。原因往往不是懒,而是流程太割裂:题目在某个网页上,解析在另一个收藏夹里,想动手写点验证代码又得切到编辑器新建文件。来回切换几次,注意力就散了。
我想要的形态很具体:打开 VSCode,侧边栏自动出现今天这道题,点一下就能看到 AI 生成的解析和延伸练习,再点一下直接把示例代码插到当前文件里跑起来。整个过程不离开编辑器,出题、讲解、动手验证形成闭环。这就是「前端每日一题」插件的核心价值,它适合三类人:正在准备前端面试的同学、想保持手感但时间碎片化的在职工程师、以及带新人想找每日练习素材的团队 leader。
要实现这个闭环,插件需要解决两件事。第一是题目数据源,得有一个稳定的题库,每天取一道,最好带分类和难度。第二是 AI 解析,题目本身只是一句话,真正有价值的是「为什么这么考」「边界情况在哪」「还能怎么变形」。如果每接一个 AI 服务就换一套 Key、换一套 SDK,维护成本会很高。所以我用 TaoToken 做统一入口,一个 Key 打通多家模型,插件里只维护一份配置。
下面我会从零把这个插件搭出来。你会看到完整的package.json配置、TaoToken 的接入方式、题目拉取与 AI 解析的串联代码,以及本地调试和常见报错的排查清单。代码都可以直接复制,改改就能跑。
2. TaoToken 统一 Key 接入:一个入口打通 AI 出题与解析
先说清楚 TaoToken 在这里扮演什么角色。它是一个模型 API 聚合入口,兼容 OpenAI 风格的接口协议。对插件开发者来说,最大的好处是:我不用在代码里为每家模型写一套适配层,只要把 Base URL 指向 TaoToken 的 API 地址,用同一个 Key 就能调用不同模型。今天想用便宜快速的模型出题,明天想用推理更强的模型写解析,改一个 Model ID 就行。
接入前你需要准备两样东西:一个 TaoToken 账号,以及一个 API Key。Key 在控制台的 API Keys 页面创建,创建后复制保存,页面关闭后就不再完整显示。这里提醒一句,Key 属于敏感凭证,不要硬编码进插件源码提交到公开仓库,后面我会讲怎么用 VSCode 的配置项来管理。
TaoToken 的 API 地址是https://taotoken.net/api,注意这个地址不带任何查询参数,直接作为 Base URL 使用。如果你用的是 OpenAI 官方 SDK,把baseURL指向它即可;如果用的是原生fetch,就拼接/chat/completions这样的路径。模型对话的入口在https://taotoken.net/api,你可以在控制台里查看当前可用的模型列表和对应的 Model ID。
为什么强调「统一 Key」这件事?因为「前端每日一题」这个场景天然需要多次调用:拉题目可能要一次结构化请求,生成解析要一次长文本请求,生成延伸练习可能又是一次。如果每次调用都换服务商,Key 管理、额度统计、错误处理都会变成负担。统一到一个入口后,插件里只需要一个getAIClient()函数,所有 AI 能力都从它出去。
还有一个实际考虑是成本可控。出题和解析对模型能力的要求不一样,出题只需要理解题目分类,解析则需要讲清楚原理和边界。你可以在配置里把这两个环节指向不同的 Model ID,用便宜模型做粗活,用强模型做精讲。这种灵活切换正是统一入口带来的便利。
配置项我建议放在 VSCode 的settings.json里,通过contributes.configuration暴露给用户。这样 Key 存在用户本地配置中,不会进代码仓库。插件读取时用vscode.workspace.getConfiguration拿值,缺失时给出明确提示,引导用户去配置。下一节我会给出完整的配置片段。
3. 可复制的插件配置:package.json 与 settings 片段
这一节是实操核心,我把关键配置拆成三块:插件的package.json声明、用户的settings.json配置、以及 AI 客户端的初始化代码。路径和字段名都按 VSCode 官方约定来,复制后基本不用改。
先看package.json里需要声明的部分。重点是contributes.configuration,它决定了用户在设置里能看到哪些选项。我设计了四个配置项:API Key、Base URL、出题模型、解析模型。Base URL 给默认值,用户一般不用动。
{ "name": "frontend-daily-question", "displayName": "前端每日一题", "description": "每天一道前端面试题,AI 生成解析与延伸练习", "version": "0.1.0", "engines": { "vscode": "^1.85.0" }, "activationEvents": [ "onView:frontendDailyQuestion.sidebar" ], "main": "./out/extension.js", "contributes": { "viewsContainers": { "activitybar": [ { "id": "frontendDailyQuestion", "title": "前端每日一题", "icon": "media/icon.svg" } ] }, "views": { "frontendDailyQuestion": [ { "id": "frontendDailyQuestion.sidebar", "name": "今日题目" } ] }, "configuration": { "title": "前端每日一题", "properties": { "frontendDailyQuestion.apiKey": { "type": "string", "default": "", "description": "TaoToken API Key,在控制台 API Keys 页面创建" }, "frontendDailyQuestion.baseUrl": { "type": "string", "default": "https://taotoken.net/api", "description": "TaoToken API 基础地址" }, "frontendDailyQuestion.questionModel": { "type": "string", "default": "gpt-4o-mini", "description": "用于出题和分类的 Model ID" }, "frontendDailyQuestion.explainModel": { "type": "string", "default": "gpt-4o", "description": "用于生成解析和延伸练习的 Model ID" } } } } }对应的用户settings.json长这样,你可以在 VSCode 里按Ctrl+Shift+P打开命令面板,输入「Open User Settings (JSON)」直接编辑:
{ "frontendDailyQuestion.apiKey": "你的 TaoToken Key", "frontendDailyQuestion.baseUrl": "https://taotoken.net/api", "frontendDailyQuestion.questionModel": "gpt-4o-mini", "frontendDailyQuestion.explainModel": "gpt-4o" }接下来是 AI 客户端的初始化。我用原生fetch封装一个最小调用函数,避免引入额外依赖。注意 Base URL 后面拼接/chat/completions,请求体和 OpenAI 协议一致。
import * as vscode from 'vscode'; interface ChatMessage { role: 'system' | 'user' | 'assistant'; content: string; } export async function callAI( messages: ChatMessage[], model: string ): Promise<string> { const config = vscode.workspace.getConfiguration('frontendDailyQuestion'); const apiKey = config.get<string>('apiKey'); const baseUrl = config.get<string>('baseUrl'); if (!apiKey) { throw new Error('未配置 TaoToken API Key,请在设置中填写 frontendDailyQuestion.apiKey'); } const response = await fetch(`${baseUrl}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model, messages, temperature: 0.7 }) }); if (!response.ok) { const text = await response.text(); throw new Error(`AI 请求失败 ${response.status}: ${text}`); } const data = await response.json(); return data.choices[0].message.content; }这段代码里有两个细节值得说。第一,Authorization头用的是Bearer加 Key,这是 OpenAI 兼容协议的标准写法。第二,错误处理里把响应体也带出来了,因为 401 和 429 的排查方式完全不同,看到原始信息能省很多时间。如果你用的是 Cline MCP 或 Codex 这类工具,它们的auth.json或 MCP 配置里同样需要填 Base URL、Key、Model ID 三件套,逻辑是一致的。
配置写完后,插件启动时应该先校验一遍。我习惯在activate函数里读一次配置,如果 Key 为空就弹一个提示,附上打开设置的按钮。这样用户不会在点击题目时才发现没配 Key。
4. 串联题目数据源与 AI 解析:从拉取到渲染的完整流程
配置就绪后,核心逻辑就是「取题 → 解析 → 渲染」三步。题目数据源我选一个公开的静态 JSON 题库,托管在 GitHub 上,每天按日期取模选一道,保证同一天所有人看到的是同一题。你也可以换成自己的题库,只要返回结构一致即可。
先定义题目类型和拉取函数:
interface Question { id: number; category: string; difficulty: 'easy' | 'medium' | 'hard'; title: string; answer: string; } const QUESTION_SOURCE = 'https://raw.githubusercontent.com/your-repo/frontend-questions/main/questions.json'; export async function fetchDailyQuestion(): Promise<Question> { const response = await fetch(QUESTION_SOURCE); if (!response.ok) { throw new Error(`题库拉取失败: ${response.status}`); } const questions: Question[] = await response.json(); const dayIndex = Math.floor(Date.now() / 86400000) % questions.length; return questions[dayIndex]; }拿到题目后,把它交给 AI 生成解析。这里的 prompt 设计很关键,我要求模型输出三部分:核心考点、常见误区、延伸练习。这样渲染出来结构清晰,用户一眼能看到重点。
export async function generateExplanation(question: Question): Promise<string> { const config = vscode.workspace.getConfiguration('frontendDailyQuestion'); const model = config.get<string>('explainModel') || 'gpt-4o'; const messages = [ { role: 'system', content: '你是一位资深前端面试官,擅长把面试题讲透。请按「核心考点」「常见误区」「延伸练习」三个小节输出,每节用 Markdown 二级标题。' }, { role: 'user', content: `题目:${question.title}\n分类:${question.category}\n难度:${question.difficulty}\n参考要点:${question.answer}` } ]; return callAI(messages, model); }渲染部分用 VSCode 的WebviewViewProvider,把 Markdown 转成 HTML 展示。为了简化,我直接用markdown-it在插件端转换,然后把 HTML 塞进 webview。侧边栏的注册代码如下:
export class DailyQuestionProvider implements vscode.WebviewViewProvider { public static readonly viewType = 'frontendDailyQuestion.sidebar'; constructor(private readonly extensionUri: vscode.Uri) {} async resolveWebviewView(webviewView: vscode.WebviewView) { webviewView.webview.options = { enableScripts: true }; try { const question = await fetchDailyQuestion(); const explanation = await generateExplanation(question); webviewView.webview.html = this.renderHtml(question, explanation); } catch (err) { webviewView.webview.html = this.renderError((err as Error).message); } } private renderHtml(question: Question, explanation: string): string { return `<!DOCTYPE html> <html> <head><meta charset="UTF-8"></head> <body> <h2>${question.title}</h2> <p>分类:${question.category} | 难度:${question.difficulty}</p> <hr/> <div>${explanation}</div> </body> </html>`; } private renderError(message: string): string { return `<!DOCTYPE html><html><body><p>加载失败:${message}</p></body></html>`; } }在activate里注册这个 provider:
export function activate(context: vscode.ExtensionContext) { const provider = new DailyQuestionProvider(context.extensionUri); context.subscriptions.push( vscode.window.registerWebviewViewProvider( DailyQuestionProvider.viewType, provider ) ); }到这里,从出题到讲解的链路就通了。用户打开侧边栏,插件自动拉题、调 AI、渲染结果。如果想加「插入示例代码到当前文件」的功能,可以在 webview 里加一个按钮,通过postMessage通知插件端执行vscode.commands.executeCommand('editor.action.insertSnippet', ...)。这个扩展点留给你自由发挥。
5. 本地运行验证清单与常见报错排查
代码写完,按 F5 启动扩展开发宿主窗口,这是验证的第一步。下面是我整理的验证清单,按顺序走一遍,基本能覆盖所有关键路径。
第一项,确认插件被激活。在扩展宿主窗口里按Ctrl+Shift+P,输入「前端每日一题」,应该能看到侧边栏视图。如果活动栏没有图标,检查package.json里的viewsContainers和views字段是否拼写正确,id是否和注册时一致。
第二项,确认配置读取正常。打开设置搜索「frontendDailyQuestion」,四个配置项都应该出现。如果 Key 没填,点击侧边栏应该看到「未配置 TaoToken API Key」的提示,而不是白屏。这一步能验证配置读取和错误处理逻辑。
第三项,确认网络请求成功。填好 Key 后重新加载窗口,侧边栏应该显示题目和解析。如果卡住不动,打开「帮助 → 切换开发人员工具」,在 Console 里看有没有报错。
下面是我实际踩过的几个报错,对照着排查会快很多。
报错一:401 Unauthorized。响应体通常是{"error":{"message":"Invalid API key"}}。原因一般是 Key 复制时带了空格,或者用了已删除的 Key。解决方法是重新在控制台创建一个 Key,粘贴时注意首尾不要有空白字符。也有可能是Authorization头拼错了,确认是Bearer加一个空格再加 Key。
报错二:local proxy failed 或连接超时。这类错误说明请求根本没到服务端。先检查baseUrl是不是https://taotoken.net/api,有没有多写或少写路径。如果公司网络有代理设置,确认 VSCode 的http.proxy配置是否正确。还有一种情况是防火墙拦截,换一个网络环境试试。
报错三:Cannot read properties of undefined (reading 'choices')。这个报错说明响应体结构不符合预期,data.choices是 undefined。常见原因是请求路径拼错了,比如把/chat/completions写成了/v1/chat/completions而 Base URL 里已经带了版本号。打印一下完整响应体,看看返回的到底是什么。另外,如果 Model ID 填错了,有些服务会返回错误对象而不是标准结构,也会触发这个报错。
报错四:OAuth 相关错误或 auth.json 解析失败。如果你同时用 Codex 或 Cline 这类工具,它们的认证文件格式和插件不同。插件的 Key 存在 VSCode 设置里,不要和auth.json混用。确认三件套齐全:Base URL 指向 TaoToken、Key 是有效的、Model ID 在可用列表里。
报错五:题目拉取失败 404。检查QUESTION_SOURCE的 URL 是否可访问,GitHub raw 地址对分支名和路径大小写敏感。如果题库文件很大,考虑加缓存,避免每次打开侧边栏都全量拉取。
排查时有一个通用技巧:在callAI函数里把请求 URL、状态码、响应体都打到 Output Channel 里。VSCode 的window.createOutputChannel可以创建一个专属日志面板,比 Console 更清晰,也方便用户反馈问题时贴日志。
6. 把每日一题变成长期习惯:接入方式与工具选择
插件跑通只是开始,真正让它产生价值的是每天打开。我自己的做法是把侧边栏固定在活动栏第一位,早上到工位第一件事就是点开看题,看完顺手把延伸练习的代码插到草稿文件里跑一遍。这个过程不超过十分钟,但坚持一个月,对事件代理、闭包、原型链这些高频考点的理解会明显不一样。
如果你想把 AI 能力用在更长期的编码场景,比如让模型持续参与项目重构或 Agent 任务,可以了解 Coding Plan 这类按周期计费的方式,适合高频调用。如果只是偶尔验证某个模型的表现,模型对话入口更轻量,直接网页里试就行。接入文档里有各语言的调用示例,遇到协议细节可以对照查。
回到插件本身,有几个可以继续打磨的方向。一是加缓存,同一天的题目和解析存到globalState里,避免重复请求。二是加「换一题」按钮,让用户在同一天内也能刷不同题目。三是把解析结果导出成 Markdown 文件,方便整理成自己的面试笔记。这些都不复杂,基于现有代码加几十行就能实现。
最后说一个我踩过的坑:不要在 webview 里直接发网络请求。webview 的 CSP 限制和跨域问题会让请求失败,正确做法是 webview 通过postMessage把意图传给插件端,由插件端发起请求再把结果传回去。这个架构一开始就定好,后面加功能会顺很多。