目标:开发 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 工具 对比表
| 特性 | Tool | Plugin |
|---|---|---|
| 扩展层级 | AI 能力层 | 系统行为层 |
| 执行时机 | AI 主动调用 | 事件驱动 |
| 权限范围 | 受限(沙箱) | 较高(接近核心) |
| 安装方式 | JSON 配置 | npm / Git / 本地 |
| 开发复杂度 | 低 | 中高 |
| 热更新 | ❌ 需重启 | ✅ 可动态加载 |
| 可拦截消息流 | ❌ | ✅ |
| 可修改 UI | ❌ | ✅ |
| 适合场景 | 单次任务调用 | 全局行为增强 |
FAQ
Q: 多个插件注册同一个钩子,执行顺序是什么?
A: 按 opencode.json 中 plugins 定义的顺序执行,先注册的插件先拦截。可通过 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 使用