MonkeyCode 插件开发实战:从零构建你的第一个 AI 编程助手扩展
引言
"好的工具应该像乐高积木——每个人都能按自己的方式拼搭出独特的作品。"
MonkeyCode 的强大不仅来自于其核心功能,更来自于其可扩展的插件系统。通过插件,你可以:
- 🎯 定制 AI 行为 — 让补全结果符合团队编码规范
- 🔌 集成外部服务 — 连接 Jira、Slack、企业内部系统
- 📊 添加新功能 — 代码度量、性能分析、安全扫描...
- 🎨 改变界面 — 自定义 UI 主题和布局
- 🤖 扩展 AI 能力 — 接入自定义模型或知识库
本教程将手把手带你从零开始构建一个完整的 MonkeyCode 插件,涵盖插件架构、API 使用、UI 开发、发布流程等全链路知识。
🎯 核心信息
- GitHub 仓库: https://github.com/monkeycode-ai/monkeycode
- 开源协议: Apache License 2.0
- 插件 API 文档: plugins.monkeycode.ai
- 插件市场: marketplace.monkeycode.ai
- 示例插件: examples/plugins
一、MonkeyCode 插件架构全景
1.1 架构概览
┌─────────────────────────────────────────────────────────────┐
│ MonkeyCode 插件系统架构 │
│ │
│ ══════════════════════════════════════════════════════ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Host Application (主程序) │ │
│ │ │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
│ │ │ Editor │ │ AI │ │ LSP │ │ │
│ │ │ Engine │ │ Engine │ │ Client │ │ │
│ │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │ │
│ │ │ │ │ │ │
│ │ ┌─────▼─────────────▼─────────────▼──────────┐ │ │
│ │ │ Plugin Runtime (运行时) │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────────────────────────┐ │ │ │
│ │ │ │ Plugin API Layer │ │ │ │
│ │ │ │ • commands │ │ │ │
│ │ │ │ • events │ │ │ │
│ │ │ │ • workspace │ │ │ │
│ │ │ │ • ui │ │ │ │
│ │ │ │ • ai │ │ │ │
│ │ │ └─────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────────────────────────┐ │ │ │
│ │ │ │ Sandbox Manager │ │ │ │
│ │ │ │ • 权限控制 │ │ │ │
│ │ │ │ • 资源隔离 │ │ │ │
│ │ │ │ • 安全审计 │ │ │ │
│ │ │ └─────────────────────────────────────┘ │ │ │
│ │ └───────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Plugins (插件实例) │ │
│ │ │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
│ │ │ Plugin A │ │ Plugin B │ │ Plugin C │ │ │
│ │ │ (Jira) │ │ (Linter) │ │ (Theme) │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ • commands │ │ • hooks │ │ • views │ │ │
│ │ │ • views │ │ • diag │ │ • styles │ │ │
│ │ └────────────┘ └────────────┘ └────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
╚═══════════════════════════════════════════════════════════╝
1.2 核心概念
| 概念 | 说明 | 类比 |
|---|---|---|
| Extension | 插件的入口点,声明元数据 | package.json |
| ActivationEvent | 触发插件激活的事件 | "什么时候启动" |
| Command | 用户可触发的命令 | 按钮点击 / 快捷键 |
| View | 插件提供的 UI 面板 | 侧边栏 / Webview |
| Hook | 在特定生命周期点执行的回调 | 中间件 |
| API | 主程序暴露给插件的接口 | SDK |
二、环境准备
2.1 初始化项目
# ===== 1. 创建插件项目 =====
mkdir monkeycode-plugin-my-first
cd monkeycode-plugin-my-first
# ===== 2. 初始化 npm 项目 =====
npm init -y
# ===== 3. 安装依赖 =====
# @monkeycode/plugin-sdk: 官方插件开发 SDK
# typescript: TypeScript 编译器
# esbuild: 快速打包工具
npm install @monkeycode/plugin-sdk
npm install -D typescript @types/node esbuild
# ===== 4. 创建基础目录结构 =====
mkdir -p src/{commands,views,hooks,utils}
2.2 项目配置文件
// ===== package.json =====
{
"name": "monkeycode-plugin-my-first",
"displayName": "My First MonkeyCode Plugin",
"description": "我的第一个 MonkeyCode 插件 — 展示基本功能",
"version": "1.0.0",
"publisher": "your-github-username",
"license": "Apache-2.0",
// 插件入口
"main": "./dist/index.js",
// 激活事件
"activationEvents": [
"onCommand:myFirstPlugin.hello",
"onLanguage:typescript",
"onLanguage:javascript"
],
// 贡献点(注册命令、视图等)
"contributes": {
"commands": [
{
"command": "myFirstPlugin.hello",
"title": "Hello World",
"category": "My First Plugin"
},
{
"command": "myFirstPlugin.showPanel",
"title": "Show Panel",
"category": "My First Plugin"
},
{
"command": "myFirstPlugin.analyzeCode",
"title": "Analyze Current File",
"category": "My First Plugin"
}
],
"keybindings": [
{
"command": "myFirstPlugin.hello",
"key": "ctrl+shift+h",
"when": "editorTextFocus"
}
],
"menus": {
"editor/context": [
{
"command": "myFirstPlugin.analyzeCode",
"group": "navigation@1",
"when": "editorTextFocus"
}
]
},
"configuration": {
"title": "My First Plugin",
"properties": {
"myFirstPlugin.greeting": {
"type": "string",
"default": "Hello from My First Plugin!",
"description": "Custom greeting message"
},
"myFirstPlugin.enableAnalytics": {
"type": "boolean",
"default": true,
"description": "Enable code analytics feature"
}
}
},
// 声明所需权限
"permissions": ["workspace", "editor", "notifications"]
},
"scripts": {
"build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --external:@monkeycode/plugin-sdk --format=cjs",
"watch": "esbuild src/index.ts --bundle --watch --platform=node --target=node18 --outfile=dist/index.js --external:@monkeycode/plugin-sdk --format=cjs",
"lint": "tsc --noEmit",
"package": "npm run build && mkdir -p out && cp -r dist package.json out/"
},
"engines": {
"monkeyCode": "^1.2.0"
},
"devDependencies": {
"@monkeycode/plugin-sdk": "latest"
}
}
// ===== tsconfig.json =====
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "out"]
}
三、编写插件代码
3.1 插件入口文件
// ===== src/index.ts =====
/**
* My First MonkeyCode Plugin
*
* 这是一个完整的示例插件,展示了:
* 1. 注册命令 (Commands)
* 2. 创建自定义面板 (Views/Webview)
* 3. 监听编辑器事件 (Hooks)
* 4. 调用 AI API (AI Integration)
* 5. 管理配置 (Configuration)
*/
import * as mc from '@monkeycode/plugin-sdk';
// 插件上下文引用(在 activate 时设置)
let context: mc.ExtensionContext | null = null;
/**
* 插件激活函数 — 当满足 activationEvents 时自动调用
*/
export async function activate(ctx: mc.ExtensionContext): Promise<void> {
context = ctx;
console.log('[MyFirstPlugin] 🚀 Plugin activated!');
// 注册所有命令
registerCommands(ctx);
// 注册事件监听器
registerEventListeners(ctx);
// 显示欢迎通知
const greeting = ctx.workspace.getConfiguration('myFirstPlugin').get<string>('greeting');
mc.window.showInformationMessage(greeting || 'Hello from My First Plugin!');
}
/**
* 插件停用函数 — 插件被禁用/卸载时调用
*/
export function deactivate(): void {
console.log('[MyFirstPlugin] 👋 Plugin deactivated');
context = null;
}
// ============================================
// 命令注册
// ============================================
function registerCommands(ctx: mc.ExtensionContext): void {
// 命令 1: Hello World
ctx.subscriptions.push(
mc.commands.registerCommand('myFirstPlugin.hello', async () => {
await handleHelloCommand();
})
);
// 命令 2: 显示面板
ctx.subscriptions.push(
mc.commands.registerCommand('myFirstPlugin.showPanel', async () => {
await showAnalyticsPanel();
})
);
// 命令 3: 分析当前文件
ctx.subscriptions.push(
mc.commands.registerCommand('myFirstPlugin.analyzeCode', async () => {
await analyzeCurrentFile();
})
);
}
async function handleHelloCommand(): Promise<void> {
const selection = mc.window.activeTextEditor?.selection;
if (selection && !selection.isEmpty) {
const selectedText = mc.window.activeTextEditor.document.getText(selection);
mc.window.showInformationMessage(`👋 你选中了 ${selectedText.length} 个字符!`);
} else {
const result = await mc.window.showQuickPick([
{ label: '$(smiley) Say Hello', description: '显示问候消息' },
{ label: '$(info) Show Info', description: '显示当前文件信息' },
{ label: '$(gear) Open Settings', description: '打开插件设置' },
], {
placeHolder: '选择一个操作...',
});
if (result) {
switch (result.label) {
case '$(smiley) Say Hello':
mc.window.showInformationMessage('🎉 Hello, Developer!');
break;
case '$(info) Show Info':
showCurrentFileInfo();
break;
case '$(gear) Open Settings':
mc.commands.executeCommand('workbench.action.openSettings', 'myFirstPlugin');
break;
}
}
}
}
function showCurrentFileInfo(): void {
const editor = mc.window.activeTextEditor;
if (!editor) {
mc.window.showWarningMessage('没有打开的编辑器');
return;
}
const doc = editor.document;
const info = `
📄 文件信息:
路径: ${doc.fileName}
语言: ${doc.languageId}
行数: ${doc.lineCount}
字符数: ${doc.getText().length}
光标位置: 第 ${editor.selection.active.line + 1} 行, 第 ${editor.selection.active.character + 1} 列
`.trim();
mc.window.showInformationMessage(info);
}
3.2 创建自定义面板(Webview)
// ===== src/views/analytics-panel.ts =====
/**
* 代码分析面板
*
* 展示当前文件的统计信息和 AI 分析结果
*/
import * as mc from '@monkeycode/plugin-sdk';
let panel: mc.WebviewPanel | null = null;
interface CodeStats {
totalLines: number;
codeLines: number;
commentLines: number;
blankLines: number;
functions: number;
classes: number;
complexity: number;
}
interface AnalysisResult {
stats: CodeStats;
suggestions: string[];
riskScore: number; // 0-10
}
export async function showAnalyticsPanel(): Promise<void> {
if (panel) {
panel.reveal();
return;
}
panel = mc.window.createWebviewPanel(
'myFirstPlugin.analytics',
'📊 代码分析面板',
mc.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true,
}
);
// 设置 Webview HTML
panel.webview.html = getWebviewContent(panel.webview);
// 处理来自 Webview 的消息
panel.webview.onDidReceiveMessage(async (message) => {
switch (message.command) {
case 'analyze':
await performAnalysis(panel!);
break;
case 'refresh':
updatePanelContent(panel!);
break;
}
}, undefined, context?.subscriptions);
// 当面板关闭时清理引用
panel.onDidDispose(() => {
panel = null;
}, undefined, context?.subscriptions);
// 初始加载
updatePanelContent(panel);
}
function getWebviewContent(webview: mc.Webview): string {
// 使用 webview.asWebviewUri() 来安全地加载本地资源
const nonce = getNonce();
return /*html*/ `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; style-src 'nonce-${nonce}'; script-src 'nonce-${nonce}';">
<title>代码分析</title>
<style nonce="${nonce}">
:root {
--primary: #6366f1;
--success: #22c55e;
--warning: #f59e0b;
--danger: #ef4444;
--bg-dark: #1e1e2e;
--bg-card: #2a2a3c;
--text-primary: #e2e8f0;
--text-secondary: #94a3b8;
--border: #3d3d5c;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg-dark);
color: var(--text-primary);
padding: 16px;
line-height: 1.6;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border);
}
.header h2 { font-size: 18px; font-weight: 600; }
.btn-group { display: flex; gap: 8px; }
button {
padding: 6px 14px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover { opacity: 0.9; transform: translateY(-1px); }
.btn-secondary {
background: var(--bg-card);
color: var(--text-secondary);
border: 1px solid var(--border);
}
.btn-secondary:hover { border-color: var(--primary); }
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
.stat-card {
background: var(--bg-card);
border-radius: 10px;
padding: 14px;
text-align: center;
border: 1px solid var(--border);
}
.stat-value {
font-size: 24px;
font-weight: 700;
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: var(--text-secondary);
}
.risk-meter {
background: var(--bg-card);
border-radius: 10px;
padding: 16px;
margin-bottom: 20px;
border: 1px solid var(--border);
}
.risk-bar-container {
height: 8px;
background: var(--bg-dark);
border-radius: 4px;
overflow: hidden;
margin-top: 8px;
}
.risk-bar {
height: 100%;
border-radius: 4px;
transition: width 0.5s ease;
}
.suggestions {
background: var(--bg-card);
border-radius: 10px;
padding: 16px;
border: 1px solid var(--border);
}
.suggestions h3 {
font-size: 15px;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 6px;
}
.suggestion-item {
padding: 8px 12px;
margin-bottom: 6px;
border-radius: 6px;
font-size: 13px;
display: flex;
align-items: flex-start;
gap: 8px;
}
.suggestion-item.info { background: rgba(99, 102, 241, 0.15); }
.suggestion-item.warning { background: rgba(245, 158, 11, 0.15); }
.suggestion-item.danger { background: rgba(239, 68, 68, 0.15); }
.loading {
text-align: center;
padding: 40px;
color: var(--text-secondary);
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 12px;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="header">
<h2>📊 代码分析</h2>
<div class="btn-group">
<button class="btn-primary" id="btnAnalyze">分析代码</button>
<button class="btn-secondary" id="btnRefresh">刷新</button>
</div>
</div>
<div id="content">
<div class="loading">
<div class="spinner"></div>
<p>等待分析...</p>
</div>
</div>
<script nonce="${nonce}">
const vscode = acquireVsCodeApi();
document.getElementById('btnAnalyze')?.addEventListener('click', () => {
vscode.postMessage({ command: 'analyze' });
});
document.getElementById('btnRefresh')?.addEventListener('click', () => {
vscode.postMessage({ command: 'refresh' });
});
function renderResults(data) {
const container = document.getElementById('content');
const riskColor = data.riskScore <= 3 ? 'var(--success)' :
data.riskScore <= 6 ? 'var(--warning)' : 'var(--danger)';
container.innerHTML = \`
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">\${data.stats.totalLines}</div>
<div class="stat-label">总行数</div>
</div>
<div class="stat-card">
<div class="stat-value">\${data.stats.codeLines}</div>
<div class="stat-label">代码行</div>
</div>
<div class="stat-card">
<div class="stat-value">\${data.stats.commentLines}</div>
<div class="stat-label">注释行</div>
</div>
<div class="stat-card">
<div class="stat-value">\${data.stats.functions}</div>
<div class="stat-label">函数数</div>
</div>
<div class="stat-card">
<div class="stat-value">\${data.stats.classes}</div>
<div class="stat-label">类数量</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color:\${riskColor}">\${data.riskScore.toFixed(1)}</div>
<div class="stat-label">风险评分</div>
</div>
</div>
<div class="risk-meter">
<div style="display:flex;justify-content:space-between;font-size:13px;">
<span>风险等级</span>
<span>\${getRiskLabel(data.riskScore)}</span>
</div>
<div class="risk-bar-container">
<div class="risk-bar" style="width:\${data.riskScore * 10}%;background:\${riskColor}"></div>
</div>
</div>
<div class="suggestions">
<h3>💡 改进建议</h3>
\${data.suggestions.map(s => \`
<div class="suggestion-item \${s.level}">
<span>\${s.level === 'danger' ? '🔴' : s.level === 'warning' ? '🟡' : '🔵'}</span>
<span>\${s.text}</span>
</div>
\`).join('')}
</div>
\`;
}
function getRiskLabel(score) {
if (score <= 3) return '✅ 低风险';
if (score <= 6) return '⚠️ 中等风险';
return '🔴 高风险';
}
window.addEventListener('message', event => {
const message = event.data;
if (message.type === 'update') {
renderResults(message.data);
} else if (message.type === 'analyzing') {
document.getElementById('content').innerHTML = \`
<div class="loading">
<div class="spinner"></div>
<p>正在分析中...</p>
</div>
\`;
}
});
</script>
</body>
</html>`;
}
function getNonce(): string {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
async function performAnalysis(panel: mc.WebviewPanel): Promise<void> {
// 发送"正在分析"状态到 Webview
panel.webview.postMessage({ type: 'analyzing' });
try {
const doc = mc.window.activeTextEditor?.document;
if (!doc) {
throw new Error('没有打开的文档');
}
const code = doc.getText();
const language = doc.languageId;
// 调用 AI 进行深度分析
const analysisResult = await analyzeWithAI(code, language);
// 发送结果到 Webview
panel.webview.postMessage({
type: 'update',
data: analysisResult,
});
} catch (error) {
panel.webview.postMessage({
type: 'error',
message: error instanceof Error ? error.message : '分析失败',
});
}
}
function updatePanelContent(panel: mc.WebviewPanel): void {
const doc = mc.window.activeTextEditor?.document;
if (!doc) return;
const stats = calculateBasicStats(doc.getText());
panel.webview.postMessage({
type: 'update',
data: {
stats,
suggestions: [{ level: 'info', text: '点击"分析代码"获取 AI 深度分析' }],
riskScore: 0,
},
});
}
function calculateBasicStats(code: string): CodeStats {
const lines = code.split('\n');
let codeLines = 0;
let commentLines = 0;
let blankLines = 0;
let functions = 0;
let classes = 0;
const inBlockComment = false;
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) {
blankLines++;
} else if (trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('*')) {
commentLines++;
} else {
codeLines++;
// 简单的函数/类检测
if (/^\s*(?:function|def|func|fn)\s+\w+/.test(trimmed)) functions++;
if (/^\s*(?:class|struct|interface|type)\s+\w+/.test(trimmed)) classes++;
}
}
return {
totalLines: lines.length,
codeLines,
commentLines,
blankLines,
functions,
classes,
complexity: Math.min(10, (functions + classes * 2) / Math.max(codeLines / 50, 1)),
};
}
3.3 AI 集成
// ===== src/utils/ai-analyzer.ts =====
/**
* 使用 MonkeyCode AI API 进行代码分析
*/
import * as mc from '@monkeycode/plugin-sdk';
interface AnalysisResult {
stats: import('../views/analytics-panel').CodeStats;
suggestions: Array<{ level: 'info' | 'warning' | 'danger'; text: string }>;
riskScore: number;
}
export async function analyzeWithAI(
code: string,
language: string,
): Promise<AnalysisResult> {
// 截取前 8000 个字符避免超长输入
const truncatedCode = code.slice(0, 8000);
// 构建 AI 分析 Prompt
const prompt = buildAnalysisPrompt(truncatedCode, language);
// 调用 MonkeyCode AI API
const response = await mc.ai.chatCompletion({
model: 'gpt-4o-mini', // 使用轻量模型进行快速分析
messages: [
{
role: 'system',
content: `你是一个专业的代码质量分析师。请分析给定的代码并返回 JSON 格式的结果。
返回格式必须严格遵循以下 JSON schema:
{
"stats": { "totalLines": number, "codeLines": number, "commentLines": number, "blankLines": number, "functions": number, "classes": number, "complexity": number },
"suggestions": [{ "level": "info|warning|danger", "text": "建议文本" }],
"riskScore": number (0-10)
}`,
},
{
role: 'user',
content: prompt,
},
],
temperature: 0.3, // 低温度以获得更确定的分析结果
max_tokens: 2000,
});
// 解析 AI 返回的结果
return parseAIResponse(response.content, code);
}
function buildAnalysisPrompt(code: string, language: string): string {
return `请分析以下 ${language} 代码的质量:
\`\`\`${language}
${code}
\`\`\`
请关注以下方面:
1. 代码复杂度和可读性
2. 潜在的性能问题
3. 安全隐患
4. 最佳实践违反情况
5. 可维护性评估
请给出具体、可操作的改进建议。`;
}
function parseAIResponse(aiOutput: string, originalCode: string): AnalysisResult {
try {
// 尝试提取 JSON(AI 可能会在输出中包含额外文字)
const jsonMatch = aiOutput.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
throw new Error('无法解析 AI 输出中的 JSON');
}
const parsed = JSON.parse(jsonMatch[0]);
return {
stats: parsed.stats || calculateFallbackStats(originalCode),
suggestions: parsed.suggestions || [],
riskScore: typeof parsed.riskScore === 'number' ? parsed.riskScore : 5,
};
} catch (error) {
console.error('[MyFirstPlugin] Failed to parse AI response:', error);
// 降级为基本统计分析
return {
stats: calculateFallbackStats(originalCode),
suggestions: [{ level: 'warning', text: 'AI 分析解析失败,仅显示基本统计' }],
riskScore: 5,
};
}
}
function calculateFallbackStats(code: string): AnalysisResult['stats'] {
const lines = code.split('\n');
let codeLines = 0, commentLines = 0, blankLines = 0;
for (const line of lines) {
const t = line.trim();
if (!t) blankLines++;
else if (/^(\/\/|#|\*|\/\*)/.test(t)) commentLines++;
else codeLines++;
}
return {
totalLines: lines.length,
codeLines,
commentLines,
blankLines,
functions: (code.match(/(?:function|def|func|fn)\s+/g) || []).length,
classes: (code.match(/(?:class|struct|interface|type)\s+/g) || []).length,
complexity: Math.min(10, lines.length / 100),
};
}
3.4 事件监听与 Hooks
// ===== src/hooks/event-listeners.ts =====
/**
* 编辑器事件监听器
*
* 监听文件保存、光标变化等事件,
* 自动触发相关操作
*/
import * as mc from '@monkeycode/plugin-sdk';
export function registerEventListeners(ctx: mc.ExtensionContext): void {
// ---- 事件 1: 文件保存时自动检查 ----
ctx.subscriptions.push(
mc.workspace.onDidSaveTextDocument(async (doc) => {
const config = mc.workspace.getConfiguration('myFirstPlugin');
const enableAnalytics = config.get<boolean>('enableAnalytics', true);
if (!enableAnalytics) return;
// 只处理支持的编程语言
const supportedLanguages = ['typescript', 'javascript', 'python', 'go', 'rust', 'java'];
if (!supportedLanguages.includes(doc.languageId)) return;
// 简单检查:检测是否有硬编码的秘密信息
const secretsDetected = detectSecrets(doc.getText());
if (secretsDetected.length > 0) {
const action = await mc.window.showWarningMessage(
`⚠️ 检测到 ${secretsDetected.length} 处可能的敏感信息!`,
{ modal: false },
'查看详情',
'忽略'
);
if (action === '查看详情') {
// 显示详情面板
showSecretsDetails(secretsDetected);
}
}
})
);
// ---- 事件 2: 光标位置变化时更新状态栏 ----
ctx.subscriptions.push(
mc.window.onDidChangeTextEditorSelection((event) => {
const editor = event.textEditor;
if (!editor) return;
const position = editor.selection.active;
const line = editor.document.lineAt(position.line);
// 更新状态栏
mc.setStatusBarMessage(
`行 ${position.line + 1}, 列 ${position.character + 1} | ` +
`字符: ${line.text.length}`,
5000 // 5秒后消失
);
})
);
// ---- 事件 3: 终端创建时注入自定义命令 ----
ctx.subscriptions.push(
mc.window.onDidOpenTerminal((terminal) => {
// 可以在终端初始化时执行自定义操作
console.log(`[MyFirstPlugin] Terminal opened: ${terminal.name}`);
})
);
}
/**
* 简单的秘密信息检测
*/
function detectSecrets(code: string): Array<{ line: number; type: string; preview: string }> {
const results: Array<{ line: number; type: string; preview: string }> = [];
const lines = code.split('\n');
const patterns = [
{ regex: /api[_-]?key\s*[:=]\s*["'][a-zA-Z0-9]{20,}["']/i, type: 'API Key' },
{ regex: /password\s*[:=]\s*["'][^"']{6,}["']/i, type: 'Password' },
{ regex: /secret\s*[:=]\s*["'][^"']{10,}["']/i, type: 'Secret' },
{ regex: /AKIA[A-Z0-9]{16}/, type: 'AWS Key' },
{ regex: /ghp_[a-zA-Z0-9]{36}/, type: 'GitHub Token' },
{ regex: /sk-[a-zA-Z0-9]{20,}/, type: 'OpenAI Key' },
];
for (let i = 0; i < lines.length; i++) {
for (const pattern of patterns) {
if (pattern.regex.test(lines[i])) {
results.push({
line: i + 1,
type: pattern.type,
preview: lines[i].trim().slice(0, 60) + (lines[i].trim().length > 60 ? '...' : ''),
});
}
}
}
return results;
}
function showSecretsDetails(secrets: Array<{ line: number; type: string; preview: string }>): void {
const details = secrets
.map(s => ` 🔴 Line ${s.line}: [${s.type}] ${s.preview}`)
.join('\n');
mc.window.showWarningMessage(
`发现潜在敏感信息:\n\n${details}\n\n建议使用环境变量或密钥管理服务存储这些值。`,
{ modal: true }
);
}
3.5 主入口整合
// ===== src/index.ts (完整版) =====
// 将所有模块整合到入口文件
import * as mc from '@monkeycode/plugin-sdk';
import { showAnalyticsPanel } from './views/analytics-panel';
import { registerEventListeners } from './hooks/event-listeners';
let context: mc.ExtensionContext | null = null;
export async function activate(ctx: mc.ExtensionContext): Promise<void> {
context = ctx;
console.log('[MyFirstPlugin] 🚀 Activated!');
registerCommands(ctx);
registerEventListeners(ctx);
const greeting = ctx.workspace.getConfiguration('myFirstPlugin')
.get<string>('greeting', 'Hello from My First Plugin!');
mc.window.showInformationMessage(greeting);
}
export function deactivate(): void {
console.log('[MyFirstPlugin] 👋 Deactivated');
context = null;
}
function registerCommands(ctx: mc.ExtensionContext): void {
ctx.subscriptions.push(
mc.commands.registerCommand('myFirstPlugin.hello', async () => {
const result = await mc.window.showQuickPick([
{ label: '$(smiley) Say Hello', description: '问候' },
{ label: '$(info) File Info', description: '文件信息' },
{ label: '$(gear) Settings', description: '设置' },
], { placeHolder: '选择操作...' });
if (result) {
switch (result.label) {
case '$(smiley) Say Hello':
mc.window.showInformationMessage('🎉 Hello!');
break;
case '$(info) File Info': {
const e = mc.window.activeTextEditor;
if (e) mc.window.showInformationMessage(
`${e.document.fileName} | ${e.document.lineCount} lines`
);
break;
}
case '$(gear) Settings':
mc.commands.executeCommand('workbench.action.openSettings', 'myFirstPlugin');
break;
}
}
})
);
ctx.subscriptions.push(
mc.commands.registerCommand('myFirstPlugin.showPanel', () =>
showAnalyticsPanel()
)
);
ctx.subscriptions.push(
mc.commands.registerCommand('myFirstPlugin.analyzeCode', () =>
showAnalyticsPanel().then(() => {/* auto analyze */})
)
);
}
四、测试与调试
4.1 本地开发调试
# ===== 开发模式 =====
# 1. 启动 watch 模式(监听文件变化自动重新编译)
npm run watch
# 2. 在 MonkeyCode 中加载插件
# 方法 A: 通过命令面板
# Ctrl+Shift+P → "Developer: Load Unpacked Extension" → 选择本项目的 out 目录
# 方法 B: 通过配置文件
# 在 ~/.config/monkeyCode/extensions.json 中添加:
# {
# "localExtensions": [
# "/path/to/your/plugin/out"
# ]
# }
# 3. 打开开发者工具查看日志
# Ctrl+Shift+I → Console 标签 → 过滤 "[MyFirstPlugin]"
4.2 单元测试
// ===== tests/analyzer.test.ts =====
import { describe, it, expect } from '@jest/globals';
import { detectSecrets } from '../src/hooks/event-listeners';
import { calculateBasicStats } from '../src/views/analytics-panel';
describe('detectSecrets', () => {
it('should detect API keys', () => {
const code = 'const apiKey = "sk-proj-abc123def456ghi789jkl012mno345pqr";';
const results = detectSecrets(code);
expect(results).toHaveLength(1);
expect(results[0].type).toBe('OpenAI Key');
});
it('should detect AWS keys', () => {
const code = 'const key = "AKIAIOSFODNN7EXAMPLE";';
const results = detectSecrets(code);
expect(results).toHaveLength(1);
expect(results[0].type).toBe('AWS Key');
});
it('should not flag normal strings', () => {
const code = 'const message = "Hello, World!";';
const results = detectSecrets(code);
expect(results).toHaveLength(0);
});
it('should detect multiple secrets in one file', () => {
const code = `
const api = "sk-proj-abc123";
const pass = "mypassword123";
const awsKey = "AKIAIOSFODNN7EXAMPLE";
`;
const results = detectSecrets(code);
expect(results.length).toBeGreaterThanOrEqual(2);
});
});
describe('calculateBasicStats', () => {
it('should count lines correctly', () => {
const code = '\n\nhello\n// comment\nworld\n';
const stats = calculateBasicStats(code);
expect(stats.totalLines).toBe(5);
expect(stats.blankLines).toBe(2);
expect(stats.commentLines).toBe(1);
expect(stats.codeLines).toBe(2);
});
it('should detect functions', () => {
const code = 'function foo() {}\nfunction bar() {}';
const stats = calculateBasicStats(code);
expect(stats.functions).toBe(2);
});
});
五、打包与发布
5.1 打包插件
# ===== 打包命令 =====
npm run package
# 生成的 out 目录结构:
# out/
# ├── dist/
# │ └── index.js # 编译后的插件代码
# ├── package.json # 插件清单
# └── README.md # 插件说明(可选)
5.2 发布到插件市场
# ===== 发布到官方市场 =====
# 1. 安装发布工具
npm install -g @monkeycode/publish-cli
# 2. 登录(首次需要)
mc publish login
# 3. 发布
mc publish ./out
# 4. 验证发布
mc publish status your-publisher-name.my-first-plugin
5.3 版本管理
# ===== VERSIONING.md =====
# MonkeyCode 插件版本规范
# 遵循 SemVer (语义化版本):
# MAJOR.MINOR.PATCH
# MAJOR: 不兼容的 API 变更
# MINOR: 向后兼容的功能新增
# PATCH: 向后兼容的问题修复
# 示例:
# v1.0.0 → 初始版本
# v1.1.0 → 新增了面板功能
# v1.1.1 → 修复了一个 bug
# v2.0.0 -> 重构了整个 API(不兼容)
六、进阶技巧
6.1 性能优化
// ===== src/utils/performance.ts =====
/**
* 插件性能优化最佳实践
*/
import * as mc from '@monkeycode/plugin-sdk';
// ✅ 1. 使用防抖避免频繁触发
export function debounce<T extends (...args: any[]) => any>(
fn: T,
delay: number,
): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout>;
return (...args: Parameters<T>) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// ✅ 2. 缓存计算结果
class LRUCache<K, V> {
private cache = new Map<K, V>();
private maxSize: number;
constructor(maxSize: number = 100) {
this.maxSize = maxSize;
}
get(key: K): V | undefined {
const value = this.cache.get(key);
if (value !== undefined) {
// LRU: 移到最后
this.cache.delete(key);
this.cache.set(key, value);
}
return value;
}
set(key: K, value: V): void {
if (this.cache.size >= this.maxSize) {
// 删除最早的条目
const firstKey = this.cache.keys().next().value;
if (firstKey !== undefined) {
this.cache.delete(firstKey);
}
}
this.cache.set(key, value);
}
}
// ✅ 3. 使用 Worker 进行耗时计算
export function createAnalysisWorker(): Worker {
return new Worker(
new URL('./analysis-worker.ts', import.meta.url),
{ type: 'module' }
);
}
// ✅ 4. 懒加载重型模块
let _heavyModule: typeof import('./heavy-module') | null = null;
async function getHeavyModule() {
if (!_heavyModule) {
_heavyModule = await import('./heavy-module');
}
return _heavyModule;
}
6.2 错误处理与日志
// ===== src/utils/error-handler.ts =====
/**
* 统一的错误处理和日志系统
*/
import * as mc from '@monkeycode/plugin-sdk';
enum LogLevel {
DEBUG = 'DEBUG',
INFO = 'INFO',
WARN = 'WARN',
ERROR = 'ERROR',
}
class PluginLogger {
private prefix = '[MyFirstPlugin]';
debug(message: string, ...args: unknown[]): void {
console.log(`${this.prefix} [DEBUG] ${message}`, ...args);
}
info(message: string, ...args: unknown[]): void {
console.log(`${this.prefix} [INFO] ${message}`, ...args);
}
warn(message: string, ...args: unknown[]): void {
console.warn(`${this.prefix} [WARN] ${message}`, ...args);
}
error(message: string, error?: unknown): void {
console.error(`${this.prefix} [ERROR] ${message}`, error);
// 可选: 发送到错误追踪服务
// sendToErrorTracking(message, error);
}
}
export const logger = new PluginLogger();
/**
* 包装异步函数,统一错误处理
*/
export function withErrorHandling<T>(
fn: () => Promise<T>,
fallback: T,
errorMessage: string = '操作失败',
): Promise<T> {
return fn().catch((error) => {
logger.error(errorMessage, error);
mc.window.showErrorMessage(`${errorMessage}: ${error instanceof Error ? error.message : String(error)}`);
return fallback;
});
}
七、完整项目结构
monkeycode-plugin-my-first/
├── src/
│ ├── index.ts # 插件入口
│ ├── commands/
│ │ └── hello-command.ts # Hello World 命令
│ ├── views/
│ │ └── analytics-panel.ts # 分析面板 (Webview)
│ ├── hooks/
│ │ └── event-listeners.ts # 事件监听器
│ └── utils/
│ ├── ai-analyzer.ts # AI 分析集成
│ ├── performance.ts # 性能优化工具
│ └── error-handler.ts # 错误处理
├── tests/
│ └── analyzer.test.ts # 单元测试
├── out/ # 打包输出
│ ├── dist/
│ │ └── index.js
│ └── package.json
├── package.json # 项目配置
├── tsconfig.json # TS 配置
├── README.md # 插件文档
├── CHANGELOG.md # 变更日志
└── LICENSE # 许可证
结语
"每一个伟大的插件都始于一行简单的代码。"
恭喜你完成了第一个 MonkeyCode 插件的开发!你已经掌握了:
- ✅ 插件项目结构和配置
- ✅ 命令注册和快捷键绑定
- ✅ Webview 面板开发
- ✅ AI API 集成
- ✅ 事件监听和 Hook 系统
- ✅ 测试和调试方法
- ✅ 打包和发布流程
下一步?发挥你的创意!无论是连接内部工具、实现团队特有的编码规范检查,还是创造全新的开发体验——MonkeyCode 的插件系统为你提供了无限可能。
💡 更多资源
- 📖 完整 API 文档
- 🧩 更多示例插件
- 💬 Discord 插件开发频道
- 🏪 插件市场
MonkeyCode 插件系统 — 你的创意,无限可能! 🐵🧩✨
浙公网安备 33010602011771号