1. 为什么要在状态栏里显示 AI 补全连接状态
写 VS Code 插件写到状态栏这一篇,很多人会停在「选中几行就显示几行」的例子上。这个例子本身没问题,但它离真实产品还有一段距离。真实场景里,状态栏最值钱的地方不是显示行数,而是显示那些用户看不见、但一旦出问题就会抓狂的后台状态。AI 补全服务就是典型:请求发出去了,用户盯着编辑器等补全,结果三秒没反应,他根本不知道是网络慢、Key 失效,还是插件压根没连上。
我试过在插件里只做「请求失败弹一次 toast」的方案,结果用户反馈很一致:弹窗一闪而过,等他想看错误信息时已经没了。后来把连接状态常驻到状态栏,问题立刻变得可观测——图标是绿的说明通道正常,变黄说明正在请求,变红说明鉴权或网络有问题,鼠标悬停还能看到最近一次错误。用户不需要读日志,扫一眼底部就知道该不该等。
这一篇要解决的核心问题是:如何用一套统一的 Key 和 API 通道,把 AI 补全服务的连接状态映射到 VS Code 状态栏上。所谓统一 Key,指的是插件不把密钥散落在多个配置项里,而是集中走一个兼容 OpenAI 协议风格的入口,这样状态检测逻辑只需要维护一条链路。TaoToken 在这里扮演的角色就是这个统一入口:它提供https://taotoken.net/api这样的 API 基址,插件侧只要按标准协议发一个轻量请求,就能判断通道是否可用。
适合谁看?如果你已经写过createStatusBarItem,知道StatusBarAlignment.Right和priority是什么意思,但还没把状态栏和真实网络请求串起来,这篇就是给你补上这一环。如果你连状态栏都还没创建过,建议先回看系列前几篇,把activate里注册命令、创建 item、绑定command的流程跑通,再来看状态联动会顺很多。
需要提前说清楚边界:状态栏只做状态展示和快捷入口,不承担密钥管理、不做请求代理、也不替代编辑器本身的补全 UI。它的职责是让「AI 服务是否可用」这件事变得可见、可点、可排查。下面从配置骨架开始,一步步把这条链路搭起来。
2. TaoToken 统一 Key 与 API 通道的前置准备
在动手改extension.ts之前,先把「插件怎么拿到 Key、怎么发请求」这件事定下来。很多插件写到最后变得难维护,就是因为 Key 的来源有七八个:有的读settings.json,有的读环境变量,有的硬编码在globalState里。统一 Key 的意思是:插件只认一个配置项,所有 AI 请求都从这一个配置项取凭证,并且都打到同一个 API 基址。
TaoToken 的接入方式对插件开发者比较友好,因为它走的是标准协议风格。你需要在插件配置里暴露两个字段:一个是 API Key,一个是 Base URL。Base URL 固定用https://taotoken.net/api,注意这里不带任何查询参数,保持干净。Key 则让用户自己填,插件不内置、不硬编码、不写进源码仓库。
先看配置骨架。在package.json的contributes.configuration里加两个属性,这样用户在设置面板里就能搜到:
{ "contributes": { "configuration": { "title": "AI Completion", "properties": { "aiCompletion.apiKey": { "type": "string", "default": "", "markdownDescription": "AI 补全服务的 API Key,在 [控制台](https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=statusbar) 创建后填入。" }, "aiCompletion.baseUrl": { "type": "string", "default": "https://taotoken.net/api", "description": "API 基址,默认使用 TaoToken 统一通道。" }, "aiCompletion.modelId": { "type": "string", "default": "claude-sonnet-4-20250514", "description": "补全使用的模型 ID。" } } } } }这三个字段就是后面所有逻辑的输入源。apiKey为空时,状态栏应该直接显示「未配置」,而不是傻等请求超时。baseUrl给默认值,用户一般不用改。modelId单独拎出来,是因为不同模型对补全延迟影响很大,状态栏的「请求中」状态时长会随模型变化。
拿到 Key 的路径很简单:打开https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=statusbar,在控制台里创建一个 Key,复制出来填进 VS Code 设置。如果你还没决定用哪个模型,可以先在https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=statusbar看一眼可用列表,再回填modelId。
这里有个容易踩的坑:不要把 Key 写进settings.json后提交到 Git。VS Code 的用户设置和远程设置是分开的,团队协作时建议把aiCompletion.apiKey放进用户级设置,工作区级设置里只保留baseUrl和modelId。插件侧读取时用vscode.workspace.getConfiguration('aiCompletion'),它会自动合并用户级和工作区级,优先级规则由 VS Code 处理,你不用自己写合并逻辑。
还有一个前置动作是确认网络出口。插件运行在扩展宿主进程里,它发请求走的是 VS Code 所在机器的网络。如果你的开发机需要走公司网络策略,先在终端里用curl验证一下通道是否可达,再写代码,能省掉大量「到底是插件写错了还是网络不通」的排查时间。
3. 可复制的 settings.json 与状态栏配置骨架
这一节给的是可以直接抄进项目的配置和代码骨架。先明确文件路径:配置写在项目根目录的.vscode/settings.json(工作区级)或用户设置里;代码写在src/extension.ts。两者配合,才能让状态栏正确反映连接状态。
先看.vscode/settings.json的完整片段。注意这里只放非敏感项,Key 留空由用户自己填:
{ "aiCompletion.baseUrl": "https://taotoken.net/api", "aiCompletion.modelId": "claude-sonnet-4-20250514", "aiCompletion.apiKey": "", "aiCompletion.statusBar.enabled": true, "aiCompletion.statusBar.pollIntervalMs": 30000 }pollIntervalMs是状态轮询间隔,默认 30 秒。不要设得太短,否则状态栏会频繁闪烁,用户会以为插件在抽风。也不要设太长,否则 Key 失效后用户要等很久才看到红色。30 秒是个比较稳的折中。
接下来是extension.ts里的状态栏骨架。核心思路是:定义一个ConnectionState枚举,状态栏的图标、颜色、tooltip 都由这个状态驱动,而不是散落在各个回调里。
import * as vscode from 'vscode'; type ConnectionState = 'unconfigured' | 'connecting' | 'connected' | 'error'; let statusBarItem: vscode.StatusBarItem; let currentState: ConnectionState = 'unconfigured'; let lastError = ''; const ICONS: Record<ConnectionState, string> = { unconfigured: '$(key)', connecting: '$(sync~spin)', connected: '$(check)', error: '$(error)' }; const COLORS: Record<ConnectionState, vscode.ThemeColor | undefined> = { unconfigured: new vscode.ThemeColor('statusBarItem.warningBackground'), connecting: undefined, connected: undefined, error: new vscode.ThemeColor('statusBarItem.errorBackground') }; function renderStatusBar(): void { const cfg = vscode.workspace.getConfiguration('aiCompletion'); const enabled = cfg.get<boolean>('statusBar.enabled', true); if (!enabled) { statusBarItem.hide(); return; } const labels: Record<ConnectionState, string> = { unconfigured: 'AI 未配置', connecting: 'AI 连接中', connected: 'AI 已连接', error: 'AI 异常' }; statusBarItem.text = `${ICONS[currentState]} ${labels[currentState]}`; statusBarItem.color = COLORS[currentState]; statusBarItem.tooltip = buildTooltip(); statusBarItem.command = 'aiCompletion.showStatusDetail'; statusBarItem.show(); } function buildTooltip(): vscode.MarkdownString { const md = new vscode.MarkdownString(); md.appendMarkdown(`**AI 补全状态**:${currentState}\n\n`); if (lastError) { md.appendMarkdown(`最近错误:\`${lastError}\`\n\n`); } md.appendMarkdown(`[打开控制台](https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=statusbar)`); return md; }这段代码里有两个细节值得说。第一,$(sync~spin)是 VS Code 内置的旋转图标,用来表示「进行中」,比静态图标更能传达「正在请求」的语义。第二,statusBarItem.color只在异常和未配置时设置背景色,正常状态不设,这样不会破坏用户主题的视觉一致性。
状态切换的入口统一收口到一个函数,避免多处直接改currentState:
function setState(next: ConnectionState, error?: string): void { currentState = next; lastError = error ?? ''; renderStatusBar(); }然后是激活逻辑。在activate里创建状态栏、注册命令、启动首次检测:
export function activate(context: vscode.ExtensionContext) { statusBarItem = vscode.window.createStatusBarItem( vscode.StatusBarAlignment.Right, 100 ); context.subscriptions.push(statusBarItem); context.subscriptions.push( vscode.commands.registerCommand('aiCompletion.showStatusDetail', async () => { const detail = await probeConnection(); vscode.window.showInformationMessage( `状态:${currentState},详情:${detail}` ); }) ); context.subscriptions.push( vscode.workspace.onDidChangeConfiguration((e) => { if (e.affectsConfiguration('aiCompletion')) { void refreshConnection(); } }) ); void refreshConnection(); const timer = setInterval(() => void refreshConnection(), 30000); context.subscriptions.push({ dispose: () => clearInterval(timer) }); }到这里,配置和骨架就齐了。refreshConnection和probeConnection是下一节的重点,它们负责真正发请求、判断状态。注意onDidChangeConfiguration这个监听:用户改完 Key 之后,状态栏应该立刻重新检测,而不是等下一个轮询周期。这个细节能显著提升「改完就能看到结果」的体验。
4. 验证请求与状态栏图标变化的完整动作
状态栏能不能反映真实连接,取决于探测请求写得对不对。探测请求的目标不是「拿到补全结果」,而是「用最小代价确认通道可用」。所以不要发一个完整的补全请求,那样又慢又费额度。更合适的做法是发一个极短的请求,只看 HTTP 状态码和响应结构。
下面这个probeConnection用fetch发一个最小请求。注意 Node 18 以上才内置fetch,VS Code 扩展宿主版本较新时可以直接用;如果你的目标版本较老,换成https模块或node-fetch即可。
async function probeConnection(): Promise<string> { const cfg = vscode.workspace.getConfiguration('aiCompletion'); const apiKey = cfg.get<string>('apiKey', '').trim(); const baseUrl = cfg.get<string>('baseUrl', 'https://taotoken.net/api').replace(/\/$/, ''); const modelId = cfg.get<string>('modelId', ''); if (!apiKey) { setState('unconfigured'); return '未填写 API Key'; } setState('connecting'); try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 8000); const resp = await fetch(`${baseUrl}/v1/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` }, body: JSON.stringify({ model: modelId, messages: [{ role: 'user', content: 'ping' }], max_tokens: 1, stream: false }), signal: controller.signal }); clearTimeout(timeout); if (resp.status === 401 || resp.status === 403) { setState('error', `鉴权失败 HTTP ${resp.status}`); return 'Key 无效或权限不足'; } if (!resp.ok) { setState('error', `HTTP ${resp.status}`); return `服务返回 ${resp.status}`; } const data = await resp.json(); if (!data || !Array.isArray(data.choices)) { setState('error', '响应结构异常'); return '响应缺少 choices 字段'; } setState('connected'); return '通道正常'; } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); if (msg.includes('aborted')) { setState('error', '请求超时'); return '8 秒内未响应'; } setState('error', msg); return msg; } } async function refreshConnection(): Promise<void> { await probeConnection(); }这段代码里有几个关键判断点,对应状态栏图标的变化:
apiKey为空 →unconfigured,图标变成钥匙,背景黄色,提示用户去配置。- 请求发出后 →
connecting,图标变成旋转的 sync,用户知道插件在工作。 - 返回 401/403 →
error,图标变成错误标记,背景红色,tooltip 显示鉴权失败。 - 返回 200 且
choices是数组 →connected,图标变成对勾,背景恢复默认。 - 超时或网络异常 →
error,tooltip 显示具体错误。
验证动作可以这样设计:先故意把apiKey清空,保存设置,观察状态栏是否立刻变成「AI 未配置」;然后填入一个错误 Key,等 30 秒或手动触发一次,看是否变红并显示 401;最后填入正确 Key,确认变绿。这一套动作走完,说明状态联动是通的。
如果你想让验证更快,可以在命令面板里加一个手动刷新命令,绑定到状态栏点击上。这样用户点一下状态栏就能立刻重测,不用等轮询。命令注册方式和前面showStatusDetail一样,把probeConnection包一层即可。
还有一个体验优化点:connecting状态如果持续太久,用户会焦虑。可以在setState('connecting')之后加一个 3 秒的软超时提示,但不要直接判失败,因为有些模型首包确实慢。状态栏的 tooltip 里可以显示「已等待 N 秒」,让用户有预期。
5. 常见报错排查:401、local proxy failed 与响应结构异常
状态栏变红之后,用户最需要的是「红在哪、怎么修」。这一节把几类高频报错和状态栏表现对应起来,方便你在插件里做更精确的提示。
第一类是401 Unauthorized。状态栏表现是红色错误图标,tooltip 显示「鉴权失败 HTTP 401」。原因通常是 Key 填错、Key 被删除、或者 Key 前后带了空格。排查动作:打开设置搜aiCompletion.apiKey,确认没有多余空格;去控制台确认 Key 还在;如果 Key 是刚创建的,确认复制完整。插件侧可以在probeConnection里对 401 单独处理,提示用户「请检查 Key 是否有效」,而不是笼统报「请求失败」。
第二类是local proxy failed。这个报错通常出现在请求根本没发出去的时候,状态栏会从connecting直接跳到error,tooltip 显示类似fetch failed或ECONNREFUSED。原因可能是本机网络策略、DNS 解析失败、或者baseUrl被改错了。排查动作:先在终端执行curl -I https://taotoken.net/api,确认基础连通性;再检查baseUrl是否被误改成带路径的地址。注意baseUrl应该是https://taotoken.net/api,后面拼接/v1/chat/completions由代码完成,不要在配置里手动加/v1。
第三类是响应缺少 choices 字段。状态栏变红,tooltip 显示「响应结构异常」。这种情况一般是请求打到了非预期端点,或者返回了错误页 HTML。排查动作:确认baseUrl没有多余斜杠,确认请求路径是/v1/chat/completions,确认Content-Type是application/json。如果返回的是 HTML,通常是路径拼错导致打到了网站首页。
第四类是OAuth 相关报错。如果你在插件里同时接了其他需要 OAuth 的服务,可能会看到OAuth token expired之类的信息。这类错误和 API Key 通道是两套体系,状态栏应该分开显示,不要混在一起。建议在ConnectionState之外再加一个维度,或者用 tooltip 区分「Key 通道」和「OAuth 通道」。
第五类是超时。状态栏长时间停在connecting,然后变红显示「请求超时」。原因可能是模型响应慢、网络抖动、或者max_tokens设得太大。探测请求里max_tokens: 1就是为了把响应压到最小,如果这样还超时,基本可以判定是网络问题。排查动作:把超时时间从 8 秒调到 15 秒再试;如果仍然超时,换一个模型 ID 试试。
为了让排查更顺,建议在插件里加一个「诊断」命令,把当前配置(Key 打码)、baseUrl、modelId、最近一次错误、耗时都打印到输出通道。用户遇到问题时,让他复制输出通道内容,比来回问「你 Key 填了吗」高效得多。
这里再强调一次配置三件套的完整性:Base URL、Key、Model ID 缺一不可。状态栏的unconfigured状态应该覆盖「Key 为空」和「Model ID 为空」两种情况,提示语要具体到缺哪个字段,而不是笼统说「未配置」。这个细节能省掉大量用户困惑。
6. 把状态栏做成 AI 补全的可观测入口
走到这里,状态栏已经不只是「显示几行选中」的小组件了,它变成了 AI 补全服务的可观测入口。用户扫一眼底部,就知道服务通不通;点一下,就能看到详情;改完配置,状态立刻刷新。这套机制的价值在于把「隐式的后台状态」变成「显式的前台信号」,减少「为什么没补全」这类无效沟通。
如果你打算继续往下做,有几个方向可以延伸。一是把状态栏和补全请求的实际耗时关联起来,显示最近一次补全的延迟,让用户对性能有感知。二是加一个「暂停 AI 补全」的开关,直接绑在状态栏点击上,用户临时不想被打扰时一键关闭。三是把状态历史记录下来,在输出通道里画一个简单的可用性时间线,方便排查间歇性故障。
需要提醒的是,状态栏的轮询请求虽然轻量,但也要注意频率。30 秒一次对大多数场景够用,如果你的用户群对实时性要求高,可以降到 15 秒,但不建议更低。另外,探测请求会消耗少量额度,虽然max_tokens: 1已经压到最低,但长期运行也要心里有数。可以在设置里给一个「关闭自动检测」的选项,让用户自己权衡。
最后回到统一 Key 这件事。插件里所有 AI 请求都从aiCompletion.apiKey和aiCompletion.baseUrl取,意味着你只需要维护一条鉴权链路、一套错误处理、一个状态机。后续要换模型、加功能,都在这条链路上扩展,不会出现「这个功能读这个 Key、那个功能读那个 Key」的混乱。状态栏作为这条链路的可视化出口,自然也就成了最稳定的那个观测点。
如果你还没拿到 Key,可以从https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=statusbar进去创建一个,填进设置后按第四节的验证动作走一遍。接入文档在https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=statusbar,里面有请求格式和字段说明,遇到响应结构问题时对照着看会快很多。状态栏跑通之后,下一步就可以把补全请求真正接进来,让这个绿色对勾背后有实际内容在流动。