目标:为 VS Code 等 IDE 开发 OpenCode 扩展
VS Code 扩展架构
开发环境准备
# 安装 Yeoman 和 VS Code 扩展生成器
npm install -g yo generator-code
# 生成扩展项目
yo code
# 选择: New Extension (TypeScript)
# 名称: opencode-helper完整扩展示例
package.json
{
"name": "opencode-helper",
"displayName": "OpenCode Helper",
"description": "OpenCode AI 编码助手",
"version": "1.0.0",
"engines": {
"vscode": "^1.85.0"
},
"categories": ["Machine Learning", "Snippets"],
"activationEvents": [
"onCommand:opencode.explain",
"onCommand:opencode.refactor"
],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "opencode.explain",
"title": "Explain with OpenCode",
"category": "OpenCode"
},
{
"command": "opencode.refactor",
"title": "Refactor with OpenCode",
"category": "OpenCode"
},
{
"command": "opencode.chat",
"title": "Open Chat",
"category": "OpenCode",
"icon": "$(comment-discussion)"
}
],
"menus": {
"editor/context": [
{
"command": "opencode.explain",
"group": "9_cutcopypaste@5",
"when": "editorHasSelection"
}
]
},
"keybindings": [
{
"command": "opencode.chat",
"key": "ctrl+shift+o",
"mac": "cmd+shift+o"
}
],
"views": {
"explorer": [
{
"id": "opencode.chatView",
"name": "OpenCode Chat",
"when": "opencode.enabled"
}
]
}
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./"
},
"devDependencies": {
"@types/vscode": "^1.85.0",
"typescript": "^5.3.0"
}
}扩展核心代码
// src/extension.ts
import * as vscode from 'vscode';
import { OpenCodeClient } from './client';
import { ChatPanel } from './chatPanel';
let client: OpenCodeClient;
export function activate(context: vscode.ExtensionContext) {
// 初始化客户端
const config = vscode.workspace.getConfiguration('opencode');
const apiKey = config.get<string>('apiKey');
if (!apiKey) {
vscode.window.showWarningMessage(
'OpenCode: 请配置 API 密钥',
'打开设置'
).then(selection => {
if (selection === '打开设置') {
vscode.commands.executeCommand('workbench.action.openSettings', 'opencode.apiKey');
}
});
return;
}
client = new OpenCodeClient({
apiKey,
baseURL: config.get<string>('baseURL') || 'https://api.opencode.ai'
});
// 注册命令
context.subscriptions.push(
vscode.commands.registerCommand('opencode.explain', explainCode),
vscode.commands.registerCommand('opencode.refactor', refactorCode),
vscode.commands.registerCommand('opencode.chat', () => ChatPanel.createOrShow(context.extensionUri, client))
);
// 注册装饰器
registerDecorators(context);
}
async function explainCode() {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
const selection = editor.selection;
const text = editor.document.getText(selection);
if (!text) {
vscode.window.showInformationMessage('请先选择一段代码');
return;
}
// 显示进度
await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: 'OpenCode 正在分析代码...',
cancellable: true
}, async (progress, token) => {
const explanation = await client.explain(text, token);
// 在输出通道显示
const channel = vscode.window.createOutputChannel('OpenCode');
channel.appendLine('=== 代码解释 ===');
channel.appendLine(explanation);
channel.show();
});
}
async function refactorCode() {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
const selection = editor.selection;
const text = editor.document.getText(selection);
const result = await client.refactor(text);
// 显示 diff
const doc = await vscode.workspace.openTextDocument({
content: result,
language: editor.document.languageId
});
await vscode.window.showTextDocument(doc, {
viewColumn: vscode.ViewColumn.Beside
});
}
function registerDecorators(context: vscode.ExtensionContext) {
const decorationType = vscode.window.createTextEditorDecorationType({
after: {
contentText: ' 🤖',
color: '#58a6ff'
}
});
// 在修改过的行添加装饰
vscode.workspace.onDidChangeTextDocument(event => {
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document !== event.document) return;
const decorations: vscode.DecorationOptions[] = [];
for (const change of event.contentChanges) {
const line = change.range.start.line;
decorations.push({
range: new vscode.Range(line, 0, line, 0)
});
}
editor.setDecorations(decorationType, decorations);
});
}
export function deactivate() {
client?.dispose();
}WebView 聊天面板
// src/chatPanel.ts
import * as vscode from 'vscode';
import { OpenCodeClient } from './client';
export class ChatPanel {
public static currentPanel: ChatPanel | undefined;
private readonly _panel: vscode.WebviewPanel;
private _disposables: vscode.Disposable[] = [];
private constructor(
panel: vscode.WebviewPanel,
private client: OpenCodeClient
) {
this._panel = panel;
this._panel.webview.html = this._getWebviewContent();
// 处理消息
this._panel.webview.onDidReceiveMessage(
async message => {
switch (message.command) {
case 'send':
const response = await this.client.send(message.text);
this._panel.webview.postMessage({
command: 'receive',
text: response
});
break;
}
},
null,
this._disposables
);
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
}
static createOrShow(extensionUri: vscode.Uri, client: OpenCodeClient) {
const column = vscode.window.activeTextEditor
? vscode.ViewColumn.Beside
: undefined;
if (ChatPanel.currentPanel) {
ChatPanel.currentPanel._panel.reveal(column);
return;
}
const panel = vscode.window.createWebviewPanel(
'opencodeChat',
'OpenCode Chat',
column || vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true
}
);
ChatPanel.currentPanel = new ChatPanel(panel, client);
}
private _getWebviewContent(): string {
return `<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: sans-serif; padding: 10px; }
#messages { height: 400px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; }
.user { color: #0066cc; margin: 5px 0; }
.ai { color: #228822; margin: 5px 0; }
#input { width: 80%; padding: 5px; }
button { padding: 5px 15px; }
</style>
</head>
<body>
<div id="messages"></div>
<input type="text" id="input" placeholder="输入消息...">
<button onclick="send()">发送</button>
<script>
const vscode = acquireVsCodeApi();
function send() {
const input = document.getElementById('input');
const text = input.value;
if (!text) return;
addMessage('user', text);
vscode.postMessage({ command: 'send', text });
input.value = '';
}
function addMessage(role, text) {
const div = document.createElement('div');
div.className = role;
div.textContent = (role === 'user' ? '你: ' : '🤖: ') + text;
document.getElementById('messages').appendChild(div);
}
window.addEventListener('message', event => {
const message = event.data;
if (message.command === 'receive') {
addMessage('ai', message.text);
}
});
</script>
</body>
</html>`;
}
dispose() {
ChatPanel.currentPanel = undefined;
this._panel.dispose();
while (this._disposables.length) {
const x = this._disposables.pop();
if (x) x.dispose();
}
}
}其他 IDE
JetBrains 插件
// Kotlin 示例
class OpenCodeAction : AnAction() {
override fun actionPerformed(e: AnActionEvent) {
val editor = e.getRequiredData(CommonDataKeys.EDITOR)
val selectedText = editor.selectionModel.selectedText
// 调用 OpenCode API
val result = OpenCodeClient.explain(selectedText)
// 显示结果
Messages.showMessageDialog(
e.project,
result,
"OpenCode 解释",
Messages.getInformationIcon()
)
}
}Neovim 插件
-- opencode.nvim
local M = {}
function M.setup(opts)
opts = opts or {}
-- 注册命令
vim.api.nvim_create_user_command('OpenCodeExplain', function()
local buf = vim.api.nvim_get_current_buf()
local start_pos = vim.api.nvim_buf_get_mark(buf, '<')
local end_pos = vim.api.nvim_buf_get_mark(buf, '>')
local lines = vim.api.nvim_buf_get_lines(buf, start_pos[1]-1, end_pos[1], false)
local text = table.concat(lines, '\n')
-- 调用 OpenCode
vim.fn.jobstart({'curl', '-s', 'https://api.opencode.ai/explain',
'-d', vim.json.encode({code = text})}, {
stdout_buffered = true,
on_stdout = function(_, data)
if data then
vim.notify(table.concat(data, '\n'), vim.log.levels.INFO)
end
end
})
end, {range = true})
end
return M调试技巧
# VS Code 扩展调试
F5 # 启动调试
Ctrl+Shift+P → "Developer: Toggle Developer Tools" # 查看 WebView 控制台下一篇:22. 企业部署与治理