Sooua
登录
返回文章列表
OpenCode··11 分钟阅读

插件系统

Plugin 是比 Tool 更强大的扩展机制。如果说 Tool 是给 AI 添加「能力」,Plugin 则是给 OpenCode 本身添加「行为」。

目标:开发 OpenCode 插件,深度定制功能


什么是插件

Plugin 是比 Tool 更强大的扩展机制。如果说 Tool 是给 AI 添加「能力」,Plugin 则是给 OpenCode 本身添加「行为」。

插件可以:

  • 修改 OpenCode 核心行为(消息拦截、响应转换)
  • 添加新界面元素(WebView 面板、状态栏)
  • 拦截和转换消息流(审计、审批、脱敏)
  • 集成外部服务(Slack 通知、企业微信告警)
  • 注册自定义命令(/deploy/review

相比工具,插件有更高的权限和灵活性,但也需要更严格的代码审查。


插件生命周期

生命周期钩子详解

阶段钩子触发时机典型用途
发现扫描插件目录验证 manifest
加载onLoad实例化插件初始化数据库连接、加载配置
激活onActivate钩子注册完成启动后台任务
运行中onMessage / onResponse / onToolCall对应事件触发核心逻辑
停用onDeactivate用户禁用插件暂停后台任务
卸载onUnload删除/更新插件清理资源、关闭连接

钩子系统详解

消息拦截钩子

// 消息审查插件 — 拦截敏感内容
async onMessage(message, next) {
  const sensitivePatterns = [
    /\b[A-Z]{2,}\d{6,}\b/,      // 身份证号
    /\b4\d{3}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/, // 信用卡
    /AKIA[0-9A-Z]{16}/           // AWS Access Key
  ];
 
  const detected = sensitivePatterns.filter(p => p.test(message.content));
 
  if (detected.length > 0) {
    // 拦截并警告
    return {
      role: 'assistant',
      content: `⚠️ 检测到可能敏感的信息(${detected.length} 处)。出于安全考虑,请避免在对话中分享密钥或个人身份信息。`
    };
  }
 
  // 正常传递
  return next(message);
}

响应后处理钩子

// 自动翻译插件 — 将 AI 响应翻译为用户语言
async onResponse(response, next) {
  const userLang = this.config.get('preferredLanguage', 'zh');
 
  if (userLang === 'en' || !response.content) {
    return next(response);
  }
 
  // 检测是否包含代码块(不翻译代码)
  const parts = response.content.split(/(```[\s\S]*?```)/);
  const translated = await Promise.all(
    parts.map(async (part) => {
      if (part.startsWith('```')) return part; // 保留代码块
      if (!part.trim()) return part;
      // 调用翻译服务
      return await translate(part, { to: userLang });
    })
  );
 
  return next({
    ...response,
    content: translated.join('')
  });
}

完整插件模板

// src/index.ts
import {
  definePlugin,
  PluginContext,
  Message,
  ToolCall,
  ToolResult
} from 'opencode';
 
export default definePlugin({
  name: 'enterprise-guard',
  version: '1.2.0',
  description: '企业级安全审计与审批插件',
  author: 'Your Company',
 
  // 声明需要的权限
  permissions: ['fs.read', 'fs.write', 'network'],
 
  // 配置 Schema(UI 中自动生成配置表单)
  configSchema: {
    type: 'object',
    properties: {
      auditWebhook: {
        type: 'string',
        format: 'uri',
        description: '审计日志 Webhook URL'
      },
      requireApprovalFor: {
        type: 'array',
        items: { type: 'string' },
        default: ['file.write', 'bash.exec'],
        description: '需要审批的操作列表'
      },
      maxTokensPerDay: {
        type: 'number',
        minimum: 1000,
        default: 500000,
        description: '每日 Token 上限'
      }
    },
    required: ['auditWebhook']
  },
 
  // 插件加载时执行
  async onLoad(context: PluginContext) {
    this.ctx = context;
    this.logger = context.logger.child({ plugin: this.name });
    this.config = context.config;
 
    // 初始化数据库/连接
    this.auditQueue = [];
    this.tokenUsage = await this.loadDailyUsage();
 
    this.logger.info('Enterprise Guard 插件已加载', {
      version: this.version,
      approvalRules: this.config.requireApprovalFor
    });
  },
 
  // 消息拦截:进入 AI 前的处理
  async onMessage(message: Message, next: Function) {
    // 1. Token 配额检查
    if (this.tokenUsage > this.config.maxTokensPerDay) {
      return {
        role: 'assistant',
        content: '⚠️ 今日 Token 配额已用完,请联系管理员调整限额或明日再试。'
      };
    }
 
    // 2. 记录用户输入审计
    this.auditQueue.push({
      type: 'user_message',
      timestamp: Date.now(),
      contentLength: message.content.length,
      user: this.ctx.session.userId
    });
 
    return next(message);
  },
 
  // AI 响应后的处理
  async onResponse(response: Message, next: Function) {
    // 统计 Token 消耗
    this.tokenUsage += response.tokens?.total || 0;
 
    // 记录响应审计
    this.auditQueue.push({
      type: 'ai_response',
      timestamp: Date.now(),
      model: response.model,
      tokens: response.tokens
    });
 
    // 批量发送审计日志
    if (this.auditQueue.length >= 10) {
      await this.flushAudit();
    }
 
    return next(response);
  },
 
  // 工具调用拦截
  async onToolCall(toolCall: ToolCall, next: Function) {
    const { name, args } = toolCall;
 
    // 检查是否需要审批
    if (this.config.requireApprovalFor.includes(name)) {
      const approved = await this.requestApproval(toolCall);
      if (!approved) {
        return {
          success: false,
          error: {
            code: 'APPROVAL_DENIED',
            message: `操作 "${name}" 未通过审批`
          }
        };
      }
    }
 
    // 记录工具调用
    this.logger.info('工具调用', { tool: name, args: Object.keys(args) });
 
    return next(toolCall);
  },
 
  // 自定义命令
  commands: {
    'guard-status': async () => {
      return `🛡️ Enterprise Guard 状态
- 今日 Token 使用: ${this.tokenUsage} / ${this.config.maxTokensPerDay}
- 待发送审计: ${this.auditQueue.length} 条
- 审批规则: ${this.config.requireApprovalFor.join(', ')}
- 版本: ${this.version}`;
    },
 
    'flush-audit': async () => {
      const count = await this.flushAudit();
      return `✅ 已强制刷新 ${count} 条审计日志`;
    }
  },
 
  // 插件卸载时清理
  async onUnload() {
    await this.flushAudit();
    this.logger.info('Enterprise Guard 插件已卸载');
  },
 
  // 内部方法
  async flushAudit() {
    if (this.auditQueue.length === 0) return 0;
    const batch = this.auditQueue.splice(0, this.auditQueue.length);
    await fetch(this.config.auditWebhook, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ batch, source: 'opencode-plugin' })
    });
    return batch.length;
  },
 
  async requestApproval(toolCall: ToolCall): Promise<boolean> {
    // 企业 IM 审批流程
    const result = await fetch(this.config.approvalWebhook, {
      method: 'POST',
      body: JSON.stringify({
        tool: toolCall.name,
        args: toolCall.args,
        requester: this.ctx.session.userId,
        timeout: 300000 // 5 分钟审批窗口
      })
    });
    const { approved } = await result.json();
    return approved;
  },
 
  async loadDailyUsage(): Promise<number> {
    // 从持久化存储加载今日用量
    try {
      const data = await this.ctx.storage.get('daily_usage');
      const today = new Date().toISOString().split('T')[0];
      return data?.date === today ? data.count : 0;
    } catch {
      return 0;
    }
  }
});

消息拦截实战:Slack 通知插件

当 AI 完成重要任务时自动通知 Slack 频道:

// src/index.ts
import { definePlugin } from 'opencode';
 
export default definePlugin({
  name: 'slack-notify',
  version: '1.0.0',
 
  async onLoad(ctx) {
    this.webhookUrl = ctx.config.get('slackWebhook');
    this.notifyOn = ctx.config.get('notifyOn', ['tool_call', 'error']);
  },
 
  async onResponse(response, next) {
    const result = await next(response);
 
    // 检测是否包含代码修改
    if (response.content?.includes('```diff') && this.notifyOn.includes('code_change')) {
      await this.sendSlack({
        text: '📝 AI 代码修改完成',
        blocks: [
          {
            type: 'section',
            text: {
              type: 'mrkdwn',
              text: `*OpenCode 代码修改*\n会话: \`${this.ctx.session.id}\`\n模型: ${response.model}`
            }
          },
          {
            type: 'context',
            elements: [
              {
                type: 'mrkdwn',
                text: `⏰ ${new Date().toLocaleString()}`
              }
            ]
          }
        ]
      });
    }
 
    return result;
  },
 
  async onToolCall(toolCall, next) {
    if (this.notifyOn.includes('tool_call')) {
      await this.sendSlack({
        text: `🔧 工具调用: ${toolCall.name}`
      });
    }
    return next(toolCall);
  },
 
  async sendSlack(payload: object) {
    await fetch(this.webhookUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload)
    });
  }
});

插件配置与发布

本地开发配置

// opencode.json
{
  "plugins": {
    "enterprise-guard": {
      "enabled": true,
      "source": "./plugins/enterprise-guard",
      "config": {
        "auditWebhook": "https://hooks.slack.com/services/xxx",
        "requireApprovalFor": ["file.write", "bash.exec", "deploy"],
        "maxTokensPerDay": 1000000
      }
    },
    "slack-notify": {
      "enabled": true,
      "source": "github:company/opencode-slack-plugin",
      "version": "^1.0.0",
      "config": {
        "slackWebhook": "${SLACK_WEBHOOK_URL}",
        "notifyOn": ["code_change", "error"]
      }
    }
  }
}

package.json 规范

{
  "name": "opencode-plugin-enterprise-guard",
  "version": "1.2.0",
  "description": "企业级安全审计与审批插件",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "engines": {
    "opencode": ">=0.5.0"
  },
  "peerDependencies": {
    "opencode": ">=0.5.0"
  },
  "keywords": [
    "opencode",
    "opencode-plugin",
    "enterprise",
    "security",
    "audit"
  ],
  "scripts": {
    "build": "tsc",
    "test": "vitest",
    "lint": "eslint src/**/*.ts"
  },
  "files": [
    "dist",
    "README.md",
    "LICENSE"
  ],
  "license": "MIT"
}

发布到 npm

# 1. 构建
npm run build
 
# 2. 测试
npm test
 
# 3. 版本升级
npm version patch  # 或 minor / major
 
# 4. 发布
npm publish --access public
 
# 5. 用户安装
opencode plugin install opencode-plugin-enterprise-guard

从 Git 安装(内网场景)

# 从私有 Git 仓库安装
opencode plugin install git+ssh://[email protected]:opencode/plugins/guard.git
 
# 指定 tag
opencode plugin install git+https://github.com/company/plugin.git#v1.2.0
 
# 本地路径(开发调试)
opencode plugin install ./my-plugin

真实场景案例

案例 1:代码审查自动分配插件

// 当 AI 检测到代码问题时,自动在 GitHub 创建 Review 并分配给相关人员
async onResponse(response, next) {
  const result = await next(response);
 
  // 检测 AI 是否提出了代码问题
  const hasIssues = /(bug|issue|problem|error|warning|⚠️|❌)/i.test(response.content);
 
  if (hasIssues && this.ctx.session.currentFile) {
    const file = this.ctx.session.currentFile;
    const owners = await this.getCodeOwners(file);
 
    await fetch('https://api.github.com/repos/org/repo/pulls/123/requested_reviewers', {
      method: 'POST',
      headers: {
        'Authorization': `token ${process.env.GITHUB_TOKEN}`,
        'Accept': 'application/vnd.github.v3+json'
      },
      body: JSON.stringify({ reviewers: owners })
    });
  }
 
  return result;
}

案例 2:成本预警插件

// 实时监控 Token 消耗,接近限额时预警
async onResponse(response, next) {
  const result = await next(response);
 
  const usage = await this.getCurrentUsage();
  const limit = this.config.monthlyBudgetUSD;
  const projected = usage.dailyAvg * 30;
 
  if (projected > limit * 0.8 && !this.warned) {
    this.warned = true;
    // 在响应前插入警告
    result.content = `⚠️ **成本预警**: 本月预计消耗 $${projected.toFixed(2)},` +
      `超出预算 $${limit} 的 80%。建议优化提示词或切换到成本更低的模型。\n\n` +
      result.content;
  }
 
  return result;
}

案例 3:多语言团队自动路由插件

// 根据用户语言自动将问题路由到对应语言专家 AI
async onMessage(message, next) {
  const detectedLang = await detectLanguage(message.content);
 
  if (detectedLang !== 'zh' && detectedLang !== 'en') {
    // 切换到对应语言的系统提示词
    this.ctx.session.systemPrompt = await this.loadPrompt(detectedLang);
  }
 
  return next(message);
}

插件 vs 工具 对比表

特性ToolPlugin
扩展层级AI 能力层系统行为层
执行时机AI 主动调用事件驱动
权限范围受限(沙箱)较高(接近核心)
安装方式JSON 配置npm / Git / 本地
开发复杂度中高
热更新❌ 需重启✅ 可动态加载
可拦截消息流
可修改 UI
适合场景单次任务调用全局行为增强

FAQ

Q: 多个插件注册同一个钩子,执行顺序是什么? A: 按 opencode.jsonplugins 定义的顺序执行,先注册的插件先拦截。可通过 priority 字段调整。

Q: 插件崩溃会影响 OpenCode 主程序吗? A: OpenCode 采用进程隔离,单个插件异常会被捕获,不会导致主程序崩溃。但建议做好 try/catch。

Q: 插件可以修改其他插件的行为吗? A: 不可以。插件之间通过 OpenCode 核心通信,不能直接互相调用。需要共享数据请使用 context.storage

Q: 如何调试插件? A: 使用 opencode --debug-plugin=your-plugin-name 启动,会在控制台输出插件级别的详细日志。

Q: 插件更新后需要重启吗? A: 本地开发插件支持热重载(文件变更自动重新加载)。npm/ Git 安装的插件需要执行 opencode plugin update


避坑清单

❌ 错误做法✅ 正确做法原因
在 onMessage 中做大量同步计算异步处理 + 必要时缓存阻塞消息处理会降低响应速度
忽略 next() 的返回值始终 return next(...)否则后续钩子和核心逻辑不会执行
在插件中存储大量内存数据使用 context.storage 持久化插件可能被卸载,内存数据丢失
不处理异步错误所有 async 操作包 try/catch未捕获的异常会导致插件被禁用
硬编码绝对路径使用 context.workspacePath不同环境路径结构不同
频繁调用外部 API批量处理 + 本地缓存减少网络开销和限流风险
插件间直接依赖通过事件总线或核心 API 通信避免循环依赖和版本冲突

下一篇:18. Go SDK 使用

分享

评论

登录 后参与讨论。

加载中…

相关文章