教程2 -- StatusBar 狀態(tài)欄與TaoToken配置實戰(zhàn))
1. 從零拆解VS Code 插件里 StatusBar 狀態(tài)欄到底能做什么VS Code 插件開發(fā)里StatusBar 狀態(tài)欄是最容易被低估的入口。它不像 Webview 那樣能畫復雜界面也不像 TreeView 那樣有層級結構但它常駐編輯器底部用戶抬眼就能看到特別適合做實時統(tǒng)計、連接狀態(tài)、模型調用提示這類輕量反饋。你如果正在搜「VS Code 插件開發(fā) StatusBar 狀態(tài)欄教程」大概率已經寫過 Hello World但卡在「怎么讓狀態(tài)欄動起來」和「插件里怎么調外部 API」這兩步。我先把這篇要交付的東西說清楚一個能自動統(tǒng)計 Markdown 字數(shù)的狀態(tài)欄插件并且在這個插件里接入 TaoToken 的統(tǒng)一 Key/API 通道讓狀態(tài)欄能顯示一次模型請求的結果。全程可復制不需要你額外搭后端。為什么選這個組合因為狀態(tài)欄插件天然適合做「異步結果展示」。你發(fā)起一個 API 請求請求回來之前狀態(tài)欄顯示加載圖標回來后顯示結果或錯誤。這個模式在真實插件里非常常見比如代碼補全、翻譯、摘要類插件都會用到。而 TaoToken 提供的是 OpenAI 兼容的 API 通道Base URL 和 Key 配好就能用省去你自己維護多模型接入的麻煩。先明確幾個概念避免后面看代碼發(fā)懵。StatusBarItem是 VS Code 提供的狀態(tài)欄條目對象通過window.createStatusBarItem()創(chuàng)建可以設置text、tooltip、command調用show()顯示、hide()隱藏、dispose()銷毀。StatusBarAlignment.Left和Right決定它出現(xiàn)在左側還是右側。狀態(tài)欄文字支持$(icon-name)語法插入官方圖標比如$(octoface)、$(sync~spin)。插件激活方式有兩種onCommand是用戶手動執(zhí)行命令才激活onLanguage:markdown是打開 Markdown 文件就激活。做實時統(tǒng)計顯然要用后者否則用戶每次都得敲命令面板體驗很差。環(huán)境準備這塊快速過一遍不展開注冊教程。終端執(zhí)行npm install -g yo generator-code然后yo code選New Extension (TypeScript)項目建好后按 F5 會彈出一個擴展開發(fā)宿主窗口在里面按CtrlShiftP輸入 Hello World 能看到右下角提示說明腳手架正常。這一步是后續(xù)所有代碼的前提。接下來我會按「先讓狀態(tài)欄顯示文字 → 再讓它自動統(tǒng)計 → 再接入 TaoToken 發(fā)起請求 → 最后排錯」的順序推進。每一步都給完整文件和配置你照著改就能跑。重點會放在package.json的貢獻點配置和WordCount.ts的類結構上因為這兩個地方最容易寫錯導致狀態(tài)欄不顯示。2. TaoToken 前置準備拿到統(tǒng)一 Key 和 API 通道在寫 API 調用代碼之前得先把通道準備好。TaoToken 的定位是統(tǒng)一 Key/API 通道你注冊后在控制臺創(chuàng)建一個 API Key就能用同一個 Key 訪問它支持的模型。對插件開發(fā)來說好處是你不用在代碼里硬編碼多個廠商的地址和密鑰換模型只改一個 Model ID。具體操作路徑打開官網 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 進入控制臺在 API Keys 頁面點創(chuàng)建復制生成的 Key。這個 Key 只顯示一次建議先存到本地臨時文件里。控制臺地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite API Keys 頁面是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。API 的基礎地址是 https://taotoken.net/api 注意這個地址不帶任何查詢參數(shù)直接作為 Base URL 使用。它兼容 OpenAI 的/v1/chat/completions路徑所以你在插件里用fetch或axios發(fā)請求時完整地址是https://taotoken.net/api/v1/chat/completions。模型 ID 這塊你可以在模型對話頁面先試一下有哪些可用模型地址是 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。選一個響應快的比如常見的對話模型把它的 Model ID 記下來后面配置里要用。如果你打算長期做編碼類插件也可以了解下 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它更適合高頻調用場景。這里要強調一個安全習慣不要把 API Key 硬編碼在extension.ts里然后提交到 Git。正確做法是用 VS Code 的context.secrets存儲或者至少放在settings.json里讓用戶自己填。本文為了演示方便會先用一個常量占位但我會在代碼注釋里標出生產環(huán)境應該怎么改。配置三件套記牢Base URL 是https://taotoken.net/apiKey 是你控制臺創(chuàng)建的那串Model ID 是你選的模型標識。這三樣在后面的請求代碼里會同時出現(xiàn)缺一個都會報錯。如果你之前配過 Claude Code 或 Cline 之類的工具邏輯是一樣的只是這里跑在插件進程里。另外提一句TaoToken 的接入文檔在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 遇到請求格式不確定的時候去查一下比盲目試錯快。文檔里會說明請求體和響應體的字段和你用 OpenAI SDK 時的結構一致。準備好這三樣之后我們就可以進入代碼環(huán)節(jié)了。下一節(jié)先寫package.json的貢獻點再寫WordCount.ts的完整類最后寫extension.ts的激活邏輯。每一段都可以直接復制。3. 可復制配置package.json 貢獻點與 WordCount.ts 完整實現(xiàn)這一節(jié)是核心我把三個文件的完整內容都列出來。你先建好項目然后逐個替換。先看package.json。關鍵是activationEvents和contributes.commands兩部分。因為我們希望打開 Markdown 就自動激活所以激活事件用onLanguage:markdown。同時保留一個手動命令方便你在非 Markdown 文件里測試狀態(tài)欄。{ name: wordcount-statusbar, displayName: WordCount StatusBar, description: 統(tǒng)計 Markdown 字數(shù)并在狀態(tài)欄展示支持 TaoToken API 調用, version: 0.0.1, engines: { vscode: ^1.80.0 }, categories: [Other], activationEvents: [ onLanguage:markdown, onCommand:extension.wordCount ], main: ./out/extension.js, contributes: { commands: [ { command: extension.wordCount, title: WordCount: 統(tǒng)計當前文檔 }, { command: extension.askTaoToken, title: WordCount: 調用 TaoToken 測試 } ], configuration: { title: WordCount StatusBar, properties: { wordCount.taoTokenKey: { type: string, default: , description: TaoToken API Key }, wordCount.modelId: { type: string, default: gpt-4o-mini, description: TaoToken 模型 ID } } } }, scripts: { vscode:prepublish: npm run compile, compile: tsc -p ./, watch: tsc -watch -p ./ }, devDependencies: { types/vscode: ^1.80.0, types/node: 18.x, typescript: ^5.1.3 } }注意configuration這一段它讓用戶可以在設置里填 Key 和 Model ID避免硬編碼。你在代碼里用vscode.workspace.getConfiguration(wordCount)讀取。接下來是WordCount.ts放在src目錄下和extension.ts同級。這個類負責狀態(tài)欄的創(chuàng)建、更新、銷毀以及 API 調用。import { window, StatusBarItem, StatusBarAlignment, TextDocument, Disposable, workspace } from vscode; export class WordCount implements Disposable { private statusBar: StatusBarItem; private disposables: Disposable[] []; constructor() { this.statusBar window.createStatusBarItem(StatusBarAlignment.Left, 100); this.statusBar.command extension.wordCount; this.statusBar.tooltip 點擊統(tǒng)計當前 Markdown 字數(shù); // 監(jiān)聽編輯器選擇變化和活動編輯器切換 this.disposables.push( window.onDidChangeTextEditorSelection(() this.updateWordCount()) ); this.disposables.push( window.onDidChangeActiveTextEditor(() this.updateWordCount()) ); this.disposables.push( workspace.onDidChangeTextDocument(() this.updateWordCount()) ); this.updateWordCount(); } public updateWordCount(): void { const editor window.activeTextEditor; if (!editor) { this.statusBar.hide(); return; } const doc: TextDocument editor.document; if (doc.languageId ! markdown) { this.statusBar.hide(); return; } const textNum doc.getText().replace(/[\r\n\s]/g, ).length; this.statusBar.text textNum 0 ? $(octoface) 暫無文字 : $(octoface) ${textNum} 字; this.statusBar.show(); } public async askTaoToken(prompt: string): Promisestring { const config workspace.getConfiguration(wordCount); const apiKey config.getstring(taoTokenKey) || ; const modelId config.getstring(modelId) || gpt-4o-mini; if (!apiKey) { window.showWarningMessage(請先在設置里配置 wordCount.taoTokenKey); return ; } this.statusBar.text $(sync~spin) 請求中...; this.statusBar.show(); try { const response await fetch(https://taotoken.net/api/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${apiKey} }, body: JSON.stringify({ model: modelId, messages: [{ role: user, content: prompt }], max_tokens: 128 }) }); if (!response.ok) { const errText await response.text(); throw new Error(HTTP ${response.status}: ${errText}); } const data: any await response.json(); const content data?.choices?.[0]?.message?.content ?? ; this.statusBar.text $(check) ${content.slice(0, 20)}; return content; } catch (err: any) { this.statusBar.text $(error) 請求失敗; window.showErrorMessage(TaoToken 調用失敗: ${err.message}); return ; } } public dispose(): void { this.statusBar.dispose(); this.disposables.forEach((d) d.dispose()); } }這里有幾個細節(jié)值得說。createStatusBarItem的第二個參數(shù)是優(yōu)先級數(shù)字越大越靠左。this.statusBar.command綁定命令 ID用戶點擊狀態(tài)欄就會觸發(fā)對應命令。onDidChangeTextDocument監(jiān)聽文檔內容變化這樣你打字時字數(shù)會實時更新不用手動執(zhí)行命令。askTaoToken方法里請求地址是https://taotoken.net/api/v1/chat/completions請求頭帶Authorization: Bearer Key請求體是標準的 OpenAI 格式。響應里取choices[0].message.content。請求過程中狀態(tài)欄顯示旋轉圖標成功顯示對勾加內容前 20 字失敗顯示錯誤圖標并彈提示。最后是extension.ts負責激活和注冊命令。import * as vscode from vscode; import { WordCount } from ./WordCount; export function activate(context: vscode.ExtensionContext) { const wordCount new WordCount(); context.subscriptions.push(wordCount); context.subscriptions.push( vscode.commands.registerCommand(extension.wordCount, () { wordCount.updateWordCount(); vscode.window.showInformationMessage(已更新狀態(tài)欄字數(shù)統(tǒng)計); }) ); context.subscriptions.push( vscode.commands.registerCommand(extension.askTaoToken, async () { const editor vscode.window.activeTextEditor; const selected editor ? editor.document.getText(editor.selection) : ; const prompt selected || 用一句話介紹 VS Code 插件開發(fā); const result await wordCount.askTaoToken(prompt); if (result) { vscode.window.showInformationMessage(result); } }) ); } export function deactivate() {}激活函數(shù)里把wordCount實例 push 到context.subscriptions插件卸載時會自動調用它的dispose()。兩個命令分別對應手動統(tǒng)計和 API 測試。到這里三個文件就齊了。按 F5 啟動擴展開發(fā)宿主打開一個.md文件左下角應該出現(xiàn)字數(shù)統(tǒng)計。按CtrlShiftP輸入WordCount: 調用 TaoToken 測試如果 Key 配好了狀態(tài)欄會先轉圈再顯示結果。4. 驗證請求從狀態(tài)欄到 API 返回的完整鏈路配置寫完之后必須驗證請求真的通了。很多人卡在「代碼沒報錯但狀態(tài)欄一直轉圈」或者「彈窗說請求失敗但不知道哪一步斷了」。這一節(jié)我拆成幾個可觀察的檢查點。第一步確認 Key 和 Model ID 已經寫進設置。打開命令面板輸入Preferences: Open Settings (UI)搜索wordCount你會看到兩個配置項TaoToken Key和Model ID。把控制臺復制的 Key 粘進去Model ID 填你選的模型。如果你更習慣改 JSON打開settings.json加這兩行{ wordCount.taoTokenKey: 你的Key, wordCount.modelId: gpt-4o-mini }第二步在擴展開發(fā)宿主里打開一個 Markdown 文件隨便打幾個字觀察左下角狀態(tài)欄。如果顯示$(octoface) N 字說明狀態(tài)欄邏輯正常。如果沒顯示檢查package.json的activationEvents是否包含onLanguage:markdown以及文件語言模式是不是 Markdown右下角能看到。第三步觸發(fā) API 調用。選中一段文字按CtrlShiftP執(zhí)行WordCount: 調用 TaoToken 測試。正常流程是狀態(tài)欄變成$(sync~spin) 請求中...大約一兩秒后變成$(check) xxx同時右下角彈出模型返回的完整內容。如果請求成功你會在彈窗里看到模型對選中文字的回應。這一步驗證了三件事Key 有效、Base URL 可達、請求體格式正確。任何一件不對都會在 catch 里被捕獲并彈錯誤。第四步驗證錯誤分支。故意把 Key 改錯一個字符再執(zhí)行命令應該看到狀態(tài)欄變成$(error) 請求失敗彈窗提示TaoToken 調用失敗: HTTP 401。這說明錯誤處理生效了。把 Key 改回來即可。這里給一個用 curl 單獨驗證通道的方法方便你排除是插件代碼問題還是通道問題curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer 你的Key \ -d { model: gpt-4o-mini, messages: [{role: user, content: 說一句你好}], max_tokens: 32 }如果 curl 能返回正常 JSON說明通道沒問題插件里報錯就是代碼問題。如果 curl 也報 401那就是 Key 不對報 404檢查路徑是不是/api/v1/chat/completions報連接超時檢查網絡。成功返回的 JSON 結構長這樣你可以對照插件里取值的路徑{ choices: [ { message: { role: assistant, content: 你好有什么可以幫你的 } } ] }插件里data?.choices?.[0]?.message?.content就是取這個content字段。用可選鏈是為了防止某一層缺失導致整個插件崩潰。驗證通過后你可以把askTaoToken的調用接到更實際的場景比如選中一段代碼讓模型解釋或者選中一段中文讓模型翻譯。狀態(tài)欄在這個過程中充當了進度指示器用戶不用盯著彈窗等。還有一點如果你在調試時改了package.json的貢獻點記得重新加載擴展開發(fā)宿主窗口否則新命令不會注冊。改.ts文件如果開了tsc -watch會自動編譯但有時需要按CtrlR重載窗口。5. 常見報錯排查401、local proxy failed、reading choices 與 OAuth這一節(jié)把插件開發(fā)加 API 調用過程中最常撞到的幾個報錯列出來每個都給定位方法和修復動作。報錯一HTTP 401 Unauthorized狀態(tài)欄顯示$(error) 請求失敗彈窗提示HTTP 401。原因基本是 Key 不對或沒傳。檢查順序設置里wordCount.taoTokenKey是否為空Key 前后有沒有多余空格復制時容易帶上請求頭是不是Authorization: Bearer Key注意 Bearer 后面有一個空格。如果你用的是環(huán)境變量方式確認變量名和讀取代碼一致。修復后重新執(zhí)行命令即可。報錯二local proxy failed 或連接被拒絕這個報錯通常出現(xiàn)在你本地配了某些網絡工具導致fetch走了錯誤的出口。插件進程繼承的是 VS Code 的網絡環(huán)境如果你系統(tǒng)層面有代理設置fetch可能會嘗試走它然后失敗。排查方法先用上一節(jié)的 curl 命令在同一個終端里試如果 curl 也失敗說明是環(huán)境問題不是代碼問題。檢查系統(tǒng)代理設置確保https://taotoken.net可以直連。VS Code 自身也有http.proxy設置如果設了代理插件請求會受影響可以在設置里搜索proxy確認。報錯三Cannot read properties of undefined (reading choices)這個報錯說明response.json()返回的對象里沒有choices字段。常見原因有三個一是請求根本沒成功但你沒檢查response.ok就直接解析二是返回的是錯誤對象比如{error: {message: ...}}三是模型 ID 寫錯了服務端返回了非預期結構。修復方式是在解析前先判斷response.ok并且打印完整響應體方便定位const raw await response.text(); console.log(TaoToken raw response:, raw); const data JSON.parse(raw);把console.log的輸出在「調試控制臺」里看就能知道服務端到底返回了什么。如果是模型 ID 錯誤換成控制臺里確認可用的 ID。報錯四OAuth 相關錯誤或 token 過期如果你之前用 Claude Code 或類似工具配過 OAuth 流程可能會殘留一些憑證文件導致插件讀取到舊的 token。這類報錯關鍵詞通常是OAuth、token expired、invalid_grant。排查方法是確認插件用的是你在設置里填的 Key而不是某個全局配置文件里的舊憑證。如果你用過 ClaudeCodeAnthropic 相關配置檢查~/.claude或項目下的配置文件是否干擾。最干凈的做法是插件里只讀workspace.getConfiguration(wordCount)不讀任何外部憑證文件。報錯五狀態(tài)欄不顯示或顯示后不更新如果狀態(tài)欄壓根不出現(xiàn)先確認activationEvents里有onLanguage:markdown并且當前文件語言模式是 Markdown。如果顯示了但打字不更新檢查是否注冊了onDidChangeTextDocument監(jiān)聽。如果更新了但數(shù)字不對檢查正則/[\r\n\s]/g是否把你想統(tǒng)計的字符也去掉了。這個正則去掉所有空白和換行只留可見字符。報錯六命令面板里找不到注冊的命令package.json的contributes.commands里必須有對應 command ID且registerCommand里的 ID 要完全一致大小寫敏感。改完package.json要重載窗口。如果命令 ID 帶了extension.前綴注冊時也要帶。把這幾類報錯對照一遍基本能覆蓋 90% 的卡點。剩下的就是網絡波動或服務端臨時問題重試即可。6. 繼續(xù)深入把狀態(tài)欄插件接到真實工作流到這里一個能統(tǒng)計字數(shù)、能調 TaoToken 的狀態(tài)欄插件已經跑通了。但狀態(tài)欄的價值不止于此它可以成為你和模型交互的輕量入口。我給你幾個可以繼續(xù)擴展的方向都是基于現(xiàn)有代碼改幾行就能實現(xiàn)的。第一個方向是把選中文字直接發(fā)給模型做翻譯或解釋。你已經有askTaoToken方法只需要在命令里把prompt換成帶指令的模板比如請把下面這段文字翻譯成英文\n${selected}。狀態(tài)欄在請求期間顯示旋轉圖標返回后顯示結果摘要完整結果用彈窗或輸出通道展示。第二個方向是做請求隊列。狀態(tài)欄只有一個如果同時發(fā)多個請求會互相覆蓋文字。你可以在WordCount類里加一個計數(shù)器請求開始時pending結束時pending--狀態(tài)欄顯示$(sync~spin) 請求中 (${pending})。這樣用戶知道還有幾個請求在跑。第三個方向是把 Key 存儲從設置遷移到context.secrets。設置里的 Key 是明文存在settings.json的多人共用機器時不夠安全。context.secrets.store(taoTokenKey, key)會加密存儲讀取用context.secrets.get。改造時把askTaoToken里的config.get換成await context.secrets.get并在激活時把context傳給WordCount構造函數(shù)。第四個方向是加超時控制。fetch默認沒有超時如果服務端卡住狀態(tài)欄會一直轉圈。用AbortController加 10 秒超時const controller new AbortController(); const timeout setTimeout(() controller.abort(), 10000); try { const response await fetch(url, { signal: controller.signal, ... }); } finally { clearTimeout(timeout); }這樣超時后 catch 會捕獲AbortError狀態(tài)欄顯示失敗用戶可以重試。第五個方向是支持多模型切換。你可以在設置里把modelId改成枚舉或者加一個命令讓用戶從列表里選。狀態(tài)欄的 tooltip 可以顯示當前使用的模型用戶鼠標懸停就能看到。這些擴展都不需要重構現(xiàn)有代碼只是在WordCount類里加方法或在extension.ts里加命令注冊。你可以按需選一個先做跑通后再加下一個。最后提醒一句插件發(fā)布前記得把package.json里的publisher、repository、icon補上README.md寫清楚配置項怎么填。API Key 相關的說明要放在顯眼位置告訴用戶去 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 創(chuàng)建。如果你想讓插件支持更多模型能力可以了解下 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它適合需要長期高頻調用的場景。代碼寫到這里狀態(tài)欄從靜態(tài)文字到實時統(tǒng)計再到 API 交互的完整鏈路就閉環(huán)了。你可以把WordCount.ts里的askTaoToken當成模板復制到其他插件項目里復用只要改 Base URL 和 Model ID 就能接不同的模型。