nkds

导航

 

MonkeyCode 插件开发实战:从零构建你的第一个 AI 编程助手扩展

引言

"好的工具应该像乐高积木——每个人都能按自己的方式拼搭出独特的作品。"

MonkeyCode 的强大不仅来自于其核心功能,更来自于其可扩展的插件系统。通过插件,你可以:

  • 🎯 定制 AI 行为 — 让补全结果符合团队编码规范
  • 🔌 集成外部服务 — 连接 Jira、Slack、企业内部系统
  • 📊 添加新功能 — 代码度量、性能分析、安全扫描...
  • 🎨 改变界面 — 自定义 UI 主题和布局
  • 🤖 扩展 AI 能力 — 接入自定义模型或知识库

本教程将手把手带你从零开始构建一个完整的 MonkeyCode 插件,涵盖插件架构、API 使用、UI 开发、发布流程等全链路知识。

🎯 核心信息


一、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 的插件系统为你提供了无限可能。

💡 更多资源

MonkeyCode 插件系统 — 你的创意,无限可能! 🐵🧩✨

posted on 2026-06-30 13:08  MonkeyCode  阅读(11)  评论(0)    收藏  举报