尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

vscode插件开发之 - TestController 实战:把测试结果面板接进 TaoToken 统一通道 1. 从测试面板到统一模型通道为什么要把 TestController 接进 TaoTokenVS 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/consoleKey 管理页面在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-keysModel 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 还是 failedasync 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.getstring(baseUrl)!; const apiKey config.getstring(apiKey)!; const modelId config.getstring(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 ): Promisestring { 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 profilecallTaoToken 里用三件套发请求。剩下的都是围绕这三件事的细节打磨。后续如果要扩展有几个方向可以继续做。一是把测试用例从硬编码改成从工作区文件解析比如读取.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把请求耗时和返回内容写到测试输出里。这样面板上点开单个用例就能看到完整链路排查问题时不用再翻控制台。
返回列表