拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

vscode插件开发之 - TestController 实战:把测试结果面板接进 TaoToken 统一通道

vscode插件开发之 - TestController 实战:把测试结果面板接进 TaoToken 统一通道

1. 从测试面板到统一模型通道:为什么要把 TestController 接进 TaoToken

VS Code 的 TestController 是测试资源管理器(Testing 视图)背后的核心 API。它负责把测试用例组织成一棵树,把运行状态(通过、失败、跳过、排队)实时反馈到面板上,还能让用户点单个用例旁边的运行按钮。简单说,它决定了你的插件在测试面板里长什么样、跑起来是什么体验。

但很多同学在写测试类插件时会遇到一个尴尬:测试逻辑本身跑通了,可一旦测试用例里需要调用大模型(比如做断言生成、结果比对、失败原因分析),Key 就散落在各处——有的写在 settings.json,有的硬编码在 extension.ts,有的走环境变量。换一个模型供应商,就要改一遍代码。我试过在一个插件里同时接三家模型,最后配置文件乱到自己都不想看。

TaoToken 在这里扮演的角色就是统一通道:一个 Base URL、一个 Key、一个模型 ID,插件里所有模型调用都走同一个入口。这样 TestController 的 run 回调里发起请求时,不用关心底层是哪家模型,只关心测试结果怎么映射到面板状态。

这篇要解决的就是这个组合场景:用 TestController 搭出测试树和结果面板,同时让插件内的模型调用走 TaoToken 统一通道。适合正在开发 VS Code 测试插件、或者想把已有插件里的模型调用收敛到一个入口的开发者。读完你能拿到可复制的 package.json 贡献点、TestController 注册与 run 回调配置,以及一次本地测试运行的完整验证流程。

核心检索词先明确:VS Code 插件开发 TestController 测试面板接入统一模型通道。下面从环境准备开始,一步步把这条链路搭起来。

2. TaoToken 前置准备:Base URL、Key 与模型 ID 三件套

在写任何 TestController 代码之前,先把模型通道的三件套准备好。这三样东西是后面所有配置的基础,缺一个请求都发不出去。

第一件是 Base URL。TaoToken 的 API 入口是https://taotoken.net/api,注意这个地址不带任何查询参数,直接作为 OpenAI 兼容接口的 base 使用。如果你用的是 Anthropic 风格的接口,路径会略有不同,但本文以 OpenAI 兼容模式为主,因为 VS Code 插件里用 fetch 或 axios 调/v1/chat/completions最顺手。

第二件是 API Key。你需要到控制台创建一个 Key,创建入口在https://taotoken.net/console,Key 管理页面在https://taotoken.net/api-keys。创建时建议给 Key 起一个能识别的名字,比如vscode-test-plugin,方便后面排查是哪个插件在调用。Key 只在创建时完整显示一次,复制后先存到安全的地方。

第三件是 Model ID。这个取决于你想用哪个模型,在模型对话页面可以查看可用模型列表,地址是https://taotoken.net/models。选一个你常用的,比如gpt-4o-mini或claude-3-5-sonnet这类,记下准确的模型 ID 字符串。注意模型 ID 是大小写敏感的,写错了会直接返回 404 或 model not found。

把这三件套整理成一张表,后面配置时直接对照:

配置项值获取位置
Base URLhttps://taotoken.net/api固定
API Keysk-...console / api-keys
Model ID如gpt-4o-minimodels 页面

这里有个容易踩的坑:Base URL 末尾不要加/v1,因为 SDK 或 fetch 调用时通常会自己拼/v1/chat/completions。如果你手动拼了/v1,最终路径会变成/v1/v1/chat/completions,直接 404。我见过不止一个同学在这里卡了半天。

另外,Key 不要硬编码在源码里提交到仓库。推荐的做法是放在 VS Code 的 SecretStorage 里,或者至少放在 settings.json 的用户配置中,通过vscode.workspace.getConfiguration读取。本文为了演示清晰,会先用配置项方式,后面再讲怎么改成 SecretStorage。

三件套准备好后,就可以进入插件工程的实际配置了。下一节从 package.json 的贡献点开始,把测试面板和配置项都声明出来。

3. 可复制配置:package.json 贡献点与 TestController 注册

这一节是全文的核心操作部分,所有代码都可以直接复制到你的插件工程里。先看 package.json 需要声明哪些贡献点。

测试相关的插件需要在contributes里声明testing贡献点,这样 VS Code 才会在测试面板里给你的插件留位置。同时把配置项也声明出来,让用户能在设置里填 Key 和模型 ID。下面是一个完整的 package.json 片段:

{ "name": "taotoken-test-controller", "displayName": "TaoToken Test Controller", "version": "0.0.1", "engines": { "vscode": "^1.85.0" }, "activationEvents": [ "onStartupFinished" ], "main": "./out/extension.js", "contributes": { "testing": [ { "id": "taotokenTestController", "label": "TaoToken Tests", "icon": "$(beaker)" } ], "commands": [ { "command": "taotokenTestController.runAll", "title": "TaoToken: Run All Tests" } ], "configuration": { "title": "TaoToken Test Controller", "properties": { "taotokenTestController.baseUrl": { "type": "string", "default": "https://taotoken.net/api", "description": "TaoToken API Base URL" }, "taotokenTestController.apiKey": { "type": "string", "default": "", "description": "TaoToken API Key" }, "taotokenTestController.modelId": { "type": "string", "default": "gpt-4o-mini", "description": "Model ID used for test assertions" } } } }, "scripts": { "compile": "tsc -p ./", "watch": "tsc -watch -p ./" }, "devDependencies": { "@types/vscode": "^1.85.0", "@types/node": "^20.0.0", "typescript": "^5.3.0" } }

注意testing贡献点里的id必须和后面createTestController的第一个参数完全一致,否则面板里会出现两个入口或者干脆不显示。activationEvents用onStartupFinished是为了让插件在启动后自动激活,测试树能尽早加载。

接下来是 extension.ts 里的 TestController 注册和 run 回调。这里把模型调用也接进来,让测试用例执行时走 TaoToken 通道。先看整体结构:

import * as vscode from 'vscode'; interface TestCase { id: string; label: string; prompt: string; expected: string; } const TEST_CASES: TestCase[] = [ { id: 'case-1', label: '加法断言', prompt: '1 + 2 等于几?只回答数字。', expected: '3' }, { id: 'case-2', label: '字符串反转', prompt: '把 "abc" 反转,只回答结果。', expected: 'cba' } ]; export function activate(context: vscode.ExtensionContext) { const controller = vscode.tests.createTestController( 'taotokenTestController', 'TaoToken Tests' ); context.subscriptions.push(controller); // 构建测试树 for (const tc of TEST_CASES) { const item = controller.createTestItem(tc.id, tc.label); controller.items.add(item); } // 注册 run 回调 controller.createRunProfile( 'Run', vscode.TestRunProfileKind.Run, async (request, token) => { await runTests(controller, request, token); } ); // 注册命令 context.subscriptions.push( vscode.commands.registerCommand('taotokenTestController.runAll', async () => { const items: vscode.TestItem[] = []; controller.items.forEach(i => items.push(i)); const request = new vscode.TestRunRequest(items); const run = controller.createTestRun(request); await runTests(controller, request, new vscode.CancellationTokenSource().token); }) ); }

上面这段完成了测试树的构建和 run profile 的注册。关键点是createRunProfile的第三个参数是回调函数,VS Code 在用户点击运行按钮时会调用它,传入request(包含要跑的用例)和token(用于取消)。

现在把模型调用接进来。runTests 函数里对每个用例发起一次 TaoToken 请求,根据返回内容是否匹配 expected 来决定 passed 还是 failed:

async function runTests( controller: vscode.TestController, request: vscode.TestRunRequest, token: vscode.CancellationToken ) { const run = controller.createTestRun(request); const config = vscode.workspace.getConfiguration('taotokenTestController'); const baseUrl = config.get<string>('baseUrl')!; const apiKey = config.get<string>('apiKey')!; const modelId = config.get<string>('modelId')!; const queue: vscode.TestItem[] = []; if (request.include) { request.include.forEach(i => queue.push(i)); } else { controller.items.forEach(i => queue.push(i)); } for (const item of queue) { if (token.isCancellationRequested) { run.skipped(item); continue; } run.started(item); const tc = TEST_CASES.find(t => t.id === item.id); if (!tc) { run.errored(item, new vscode.TestMessage('未找到用例定义')); continue; } try { const actual = await callTaoToken(baseUrl, apiKey, modelId, tc.prompt); if (actual.trim() === tc.expected) { run.passed(item); } else { run.failed( item, new vscode.TestMessage(`期望 ${tc.expected},实际 ${actual.trim()}`) ); } } catch (err: any) { run.errored(item, new vscode.TestMessage(String(err.message || err))); } } run.end(); }

callTaoToken 就是实际的 HTTP 请求,走 OpenAI 兼容的 chat completions 接口:

async function callTaoToken( baseUrl: string, apiKey: string, modelId: string, prompt: string ): Promise<string> { const url = `${baseUrl.replace(/\/$/, '')}/v1/chat/completions`; const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: modelId, messages: [{ role: 'user', content: prompt }], temperature: 0 }) }); if (!res.ok) { const text = await res.text(); throw new Error(`HTTP ${res.status}: ${text}`); } const data: any = await res.json(); return data.choices?.[0]?.message?.content ?? ''; }

这里baseUrl.replace(/\/$/, '')是为了防止用户配置时末尾多打了斜杠,导致路径变成//v1/chat/completions。temperature: 0是为了让测试结果稳定,避免模型随机性导致断言时好时坏。

如果你更习惯用配置文件而不是 settings,也可以把三件套写进一个 JSON 文件,比如.taotoken/config.json:

{ "baseUrl": "https://taotoken.net/api", "apiKey": "sk-your-key-here", "modelId": "gpt-4o-mini" }

然后在插件里用vscode.workspace.fs.readFile读取。这种方式适合团队共享配置,但 Key 不要提交到仓库,建议加进.gitignore。

配置和代码都就位后,下一节做一次实际的本地运行验证,看看测试面板里能不能正确显示通过和失败。

4. 验证请求与成功结果:本地跑一次测试面板

代码写完后,按 F5 启动扩展开发宿主(Extension Development Host),会弹出一个新的 VS Code 窗口,标题栏带[Extension Development Host]。在这个窗口里打开命令面板,输入TaoToken: Run All Tests,或者直接点左侧活动栏的测试图标(烧杯形状),都能触发测试运行。

先确认配置项已经填好。在扩展开发宿主窗口里按Ctrl+,打开设置,搜索taotokenTestController,把 API Key 填进去。Base URL 和 Model ID 用默认值即可。填完后回到测试面板,应该能看到两个用例:加法断言和字符串反转。

点击测试面板顶部的运行按钮,观察每个用例的状态变化。正常情况下,加法断言会变成绿色对勾(通过),字符串反转也会通过。如果模型返回的内容带了多余的解释文字,比如「答案是 3」而不是纯3,断言就会失败,面板上显示红色叉号,鼠标悬停能看到期望值和实际值的对比。

这里有个细节值得注意:run.started(item)之后,面板上该用例会显示转圈状态,直到run.passed或run.failed被调用。如果你发现用例一直转圈不结束,大概率是 fetch 请求卡住了,检查网络和 Base URL 是否正确。

为了验证失败路径,可以临时把 TEST_CASES 里 case-1 的 expected 改成4,重新编译运行。这时加法断言应该显示失败,消息里会写「期望 4,实际 3」。这个失败信息就是通过vscode.TestMessage传进去的,面板会自动渲染。

成功运行后,测试面板顶部会显示汇总信息,比如「2 passed, 0 failed」。点单个用例还能看到它的执行时间。如果你在 runTests 里加了日志输出,可以在「输出」面板选择对应的通道查看每次请求的耗时和返回内容。

实测下来,从点击运行到两个用例都出结果,通常在 2 到 5 秒之间,取决于模型响应速度。如果超过 10 秒还没结果,先检查是不是 Key 没填或者 Base URL 写错了。

验证通过后,说明 TestController 的测试树、run 回调、TaoToken 通道这三者已经串起来了。下一节整理几个常见的报错和排查方法。

5. 本篇常见错排查:401、local proxy failed 与 reading choices

这一节把实际开发中最容易撞上的几个报错列出来,对照着排查能省不少时间。

401 Unauthorized。这是最常见的,返回体通常是{"error":{"message":"Invalid API key"}}。原因有三个:Key 没填、Key 填错、Key 前面多了Bearer前缀。注意代码里已经拼了Bearer ${apiKey},所以配置项里只填sk-...本身,不要再带Bearer。另外检查一下是不是把 Key 填到了别的配置项里,比如误填到 modelId。

local proxy failed / ECONNREFUSED。这个报错说明请求根本没发出去,卡在本地网络层。常见原因是 Base URL 写成了http://localhost:xxxx或者某个本地代理地址。本文场景下 Base URL 应该是https://taotoken.net/api,不要改成任何本地地址。如果你之前配过系统代理,检查一下 VS Code 的http.proxy设置是不是指向了一个已经关闭的端口。

Cannot read properties of undefined (reading 'choices')。这个报错发生在解析响应时,data.choices是 undefined。原因通常是返回体不是预期的 OpenAI 格式,比如返回了一个错误对象但 HTTP 状态码是 200。排查方法是在res.json()之后先打印一下data,看看实际返回了什么。另一种可能是模型 ID 写错了,某些网关在模型不存在时会返回一个非标准结构。确认 modelId 和 models 页面列出的完全一致。

OAuth / token expired 类报错。如果你用的是需要 OAuth 的模型通道,可能会看到 token 过期提示。本文用的是 API Key 方式,不涉及 OAuth 流程。如果确实看到这类报错,检查是不是误用了某个需要登录的端点。TaoToken 的 API Key 方式不需要额外的 OAuth 步骤。

测试面板不显示用例。代码跑起来了但测试视图里空空如也。先检查 package.json 里testing贡献点的id和createTestController的第一个参数是否一致。再检查activationEvents是否包含onStartupFinished,否则插件可能没激活。最后确认controller.items.add(item)确实被调用了,可以在构建测试树的地方加一行console.log。

用例一直转圈不结束。前面提过,通常是 fetch 卡住。可以在 callTaoToken 里加一个超时控制:

const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30000); try { const res = await fetch(url, { signal: controller.signal, ... }); // ... } finally { clearTimeout(timeout); }

这样 30 秒没响应就会抛错,run.errored 会捕获并显示在面板上,不会一直转圈。

Key 泄露风险。如果你把 Key 写在了 settings.json 的用户配置里,注意这个文件可能被同步到其他机器。更安全的做法是用context.secrets.store('taotokenKey', apiKey)存到 SecretStorage,读取时用context.secrets.get。这样 Key 不会出现在任何明文配置文件里。

排查完这些,基本能覆盖 90% 的接入问题。如果还是不通,把run.errored里的完整错误消息复制出来,对照 HTTP 状态码和返回体定位。

6. 把通道固定下来:后续开发与 CTA

走到这里,TestController 的测试树、run 回调、TaoToken 统一通道已经完整跑通了一次。回头看,核心其实就三件事:package.json 里声明 testing 贡献点和配置项,extension.ts 里注册 controller 和 run profile,callTaoToken 里用三件套发请求。剩下的都是围绕这三件事的细节打磨。

后续如果要扩展,有几个方向可以继续做。一是把测试用例从硬编码改成从工作区文件解析,比如读取.taotoken/tests.json,这样不用改代码就能加用例。二是把 run 回调里的串行请求改成并发,用Promise.all同时跑多个用例,面板上会看到多个用例同时转圈,整体耗时更短。三是把失败用例的实际返回内容存下来,方便后续做回归对比。

如果你在接入过程中遇到 401 或 local proxy failed 这类报错,可以直接到 API Keys 页面重新生成一个 Key 试试,地址是https://taotoken.net/api-keys。接入相关的文档在https://taotoken.net/doc,里面有各语言 SDK 的调用示例。想先验证模型通道是否通,可以到模型对话页面发一条消息试试,地址是https://taotoken.net/models。如果这个插件是长期维护的编码工具,考虑用 Coding Plan 把额度固定下来,入口在https://taotoken.net/coding-plan。

最后留一个实用技巧:在 runTests 里给每个用例加一个run.appendOutput,把请求耗时和返回内容写到测试输出里。这样面板上点开单个用例就能看到完整链路,排查问题时不用再翻控制台。

返回列表