目标:为 OpenCode 编写专用工具,扩展 AI 能力
什么是自定义工具
当内置工具无法满足需求时,你可以编写自己的 Tool。OpenCode 的工具系统遵循 MCP (Model Context Protocol) 规范,允许 AI 在对话中安全地调用外部能力。
使用场景:
- 调用内部 API(文档系统、工单平台、CI/CD)
- 执行特定业务逻辑(代码规范检查、安全扫描)
- 集成内部系统(ERP、CRM、监控系统)
- 自定义代码分析(AST 解析、依赖分析)
工具注册架构
工具类型详解
1. Script Tool(脚本工具)
最简单的方式,调用外部脚本或命令:
// opencode.json
{
"tools": {
"deploy": {
"command": "./scripts/deploy.sh",
"description": "部署服务到生产环境",
"parameters": {
"environment": {
"type": "string",
"enum": ["staging", "production"],
"description": "部署目标环境"
}
}
}
}
}完整示例 — 数据库备份脚本工具:
{
"tools": {
"db-backup": {
"command": "pg_dump",
"args": [
"-h", "{{host}}",
"-U", "{{username}}",
"-d", "{{database}}",
"-f", "/backups/{{database}}_{{timestamp}}.sql"
],
"description": "备份 PostgreSQL 数据库",
"parameters": {
"host": {
"type": "string",
"description": "数据库主机地址"
},
"username": {
"type": "string",
"description": "数据库用户名"
},
"database": {
"type": "string",
"description": "数据库名称"
},
"timestamp": {
"type": "string",
"description": "备份时间戳",
"default": "auto"
}
},
"env": {
"PGPASSWORD": "${DB_PASSWORD}"
},
"timeout": 300000
}
}
}#!/usr/bin/env bash
# scripts/deploy.sh
set -euo pipefail
ENVIRONMENT="$1"
VERSION="${2:-latest}"
echo "🚀 开始部署到 ${ENVIRONMENT},版本: ${VERSION}"
# 预检查
if "$ENVIRONMENT" == "production"; then
read -p "确认部署到生产环境? (yes/no) " confirm
if "$confirm" != "yes"; then
echo "❌ 部署已取消"
exit 1
fi
fi
# 执行部署
kubectl set image deployment/app \
app=registry.company.com/app:${VERSION} \
-n ${ENVIRONMENT}
# 等待滚动更新
kubectl rollout status deployment/app -n ${ENVIRONMENT}
echo "✅ 部署完成"2. HTTP Tool(HTTP 工具)
调用 REST API,适合集成第三方服务:
{
"tools": {
"weather": {
"url": "https://api.weather.com/v1/current",
"method": "GET",
"headers": {
"Authorization": "Bearer ${WEATHER_API_KEY}",
"Content-Type": "application/json"
},
"parameters": {
"city": {
"type": "string",
"description": "城市名称"
}
},
"timeout": 10000
}
}
}完整示例 — Jira 工单创建工具:
{
"tools": {
"jira-create": {
"url": "https://company.atlassian.net/rest/api/3/issue",
"method": "POST",
"headers": {
"Authorization": "Basic ${JIRA_BASE64_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json"
},
"body": {
"fields": {
"project": {
"key": "{{project}}"
},
"summary": "{{summary}}",
"description": {
"type": "doc",
"version": 1,
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "{{description}}"
}
]
}
]
},
"issuetype": {
"name": "{{issue_type}}"
},
"priority": {
"name": "{{priority}}"
}
}
},
"parameters": {
"project": {
"type": "string",
"description": "项目 Key,如 PROJ"
},
"summary": {
"type": "string",
"description": "工单标题"
},
"description": {
"type": "string",
"description": "工单详细描述"
},
"issue_type": {
"type": "string",
"enum": ["Bug", "Task", "Story", "Epic"],
"default": "Task",
"description": "工单类型"
},
"priority": {
"type": "string",
"enum": ["Highest", "High", "Medium", "Low", "Lowest"],
"default": "Medium",
"description": "优先级"
}
}
}
}
}3. Function Tool(函数工具)
使用 TypeScript/JavaScript 编写,最灵活的方式:
// .opencode/tools/code-analyzer.ts
import { defineTool } from 'opencode';
import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
export default defineTool({
name: 'analyze-imports',
description: '分析文件中的 import 依赖关系',
parameters: {
filePath: {
type: 'string',
description: '要分析的源代码文件路径'
},
includeExternal: {
type: 'boolean',
description: '是否包含外部依赖',
default: false
}
},
async execute({ filePath, includeExternal }) {
const fs = await import('fs/promises');
const code = await fs.readFile(filePath, 'utf-8');
const ast = parse(code, {
sourceType: 'module',
plugins: ['typescript', 'jsx']
});
const imports: string[] = [];
traverse(ast, {
ImportDeclaration(path) {
const source = path.node.source.value;
if (includeExternal || source.startsWith('.')) {
imports.push(source);
}
}
});
return {
file: filePath,
importCount: imports.length,
imports: imports.sort(),
circularRisk: imports.filter(i => i.includes(filePath.split('/').pop()!.replace('.ts', ''))).length > 0
};
}
});参数校验 Schema
OpenCode 使用 JSON Schema 校验工具参数。完整的 schema 支持:
{
"tools": {
"advanced-search": {
"command": "node",
"args": ["scripts/search.js"],
"parameters": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["query", "index"],
"properties": {
"query": {
"type": "string",
"minLength": 2,
"maxLength": 200,
"description": "搜索关键词"
},
"index": {
"type": "string",
"enum": ["code", "docs", "tickets"],
"description": "搜索索引"
},
"filters": {
"type": "object",
"properties": {
"dateRange": {
"type": "array",
"items": { "type": "string", "format": "date" },
"minItems": 2,
"maxItems": 2
},
"author": {
"type": "string",
"pattern": "^[a-zA-Z0-9_-]+$"
}
}
},
"options": {
"type": "object",
"properties": {
"highlight": { "type": "boolean", "default": true },
"pageSize": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 20
}
}
}
}
}
}
}
}错误处理
工具应始终返回结构化错误,帮助 AI 理解并修复问题:
// .opencode/tools/safe-deploy.ts
export default defineTool({
name: 'safe-deploy',
description: '安全部署服务(带预检查)',
parameters: {
service: { type: 'string', required: true },
version: { type: 'string', default: 'latest' }
},
async execute({ service, version }) {
try {
// 1. 健康检查
const health = await checkHealth(service);
if (!health.healthy) {
return {
success: false,
error: {
code: 'HEALTH_CHECK_FAILED',
message: `服务 ${service} 当前不健康`,
details: health.checks,
suggestion: '请先修复服务健康问题再部署'
}
};
}
// 2. 执行部署
const deployResult = await deploy(service, version);
return {
success: true,
data: deployResult,
summary: `✅ ${service}@${version} 部署成功`
};
} catch (error) {
return {
success: false,
error: {
code: 'DEPLOY_FAILED',
message: error instanceof Error ? error.message : '未知错误',
stack: process.env.NODE_ENV === 'development' ? (error as Error).stack : undefined,
suggestion: '请检查 CI 日志或联系 SRE 团队'
}
};
}
}
});工具权限集成
{
"permissions": {
"tools": {
"deploy": "ask", // 高危操作:每次确认
"db-backup": "ask", // 数据操作:每次确认
"weather": "allow", // 只读查询:直接执行
"jira-create": "ask", // 外部写入:每次确认
"analyze-imports": "allow" // 本地分析:直接执行
},
// 按用户组细粒度控制
"groups": {
"sre": {
"deploy": "allow",
"db-backup": "allow"
},
"developer": {
"deploy": "ask",
"db-backup": "deny"
}
}
}
}真实场景案例
案例 1:内部文档搜索工具
某互联网公司内部有 Confluence + GitLab Wiki,开发团队需要 AI 能搜索内部文档回答技术问题。
#!/usr/bin/env python3
# scripts/search-docs.py
import sys
import json
import requests
from urllib.parse import quote
def search_docs(query: str, index: str = "all", max_results: int = 5):
api_base = "https://search.internal.company.com"
payload = {
"query": query,
"index": index,
"filters": {
"visibility": "internal",
"status": "published"
},
"highlight": True,
"size": max_results
}
try:
resp = requests.post(
f"{api_base}/api/v1/search",
json=payload,
headers={"Authorization": f"Bearer {os.environ['SEARCH_API_KEY']}"},
timeout=10
)
resp.raise_for_status()
data = resp.json()
results = []
for hit in data.get("hits", []):
results.append({
"title": hit["title"],
"url": hit["url"],
"snippet": hit.get("highlight", "")[:300],
"lastUpdated": hit["updated_at"],
"author": hit["author"]
})
return {"success": True, "results": results, "total": data.get("total", 0)}
except requests.Timeout:
return {"success": False, "error": "搜索超时,请稍后重试"}
except Exception as e:
return {"success": False, "error": str(e)}
if __name__ == "__main__":
import os
query = sys.argv[1] if len(sys.argv) > 1 else ""
print(json.dumps(search_docs(query), ensure_ascii=False))// opencode.json 配置
{
"tools": {
"search-docs": {
"command": "python3",
"args": ["scripts/search-docs.py", "{{query}}"],
"parameters": {
"query": {
"type": "string",
"description": "搜索关键词"
}
}
}
}
}案例 2:Git Commit Message 生成工具
// .opencode/tools/generate-commit.ts
import { defineTool } from 'opencode';
import { execSync } from 'child_process';
export default defineTool({
name: 'generate-commit',
description: '根据 staged changes 生成符合 Conventional Commits 规范的提交信息',
parameters: {
type: {
type: 'string',
"enum": ["auto", "feat", "fix", "docs", "style", "refactor", "test", "chore"],
default: "auto",
description: '提交类型'
},
scope: {
type: 'string',
description: '影响范围,如 auth、api、ui'
}
},
async execute({ type, scope }) {
// 获取 staged diff
const diff = execSync('git diff --cached --stat', { encoding: 'utf-8' });
const files = execSync('git diff --cached --name-only', { encoding: 'utf-8' }).trim().split('\n');
// 自动推断类型
let inferredType = type;
if (type === 'auto') {
if (files.some(f => f.includes('test'))) inferredType = 'test';
else if (files.some(f => f.includes('doc'))) inferredType = 'docs';
else inferredType = 'feat';
}
const scopeStr = scope ? `(${scope})` : '';
return {
suggestion: `${inferredType}${scopeStr}: ${generateSummary(files, diff)}`,
filesChanged: files.length,
diffSummary: diff,
instructions: '请审阅后使用 git commit -m "..." 提交'
};
}
});
function generateSummary(files: string[], diff: string): string {
const mainFile = files[0]?.split('/').pop() || 'changes';
const additions = (diff.match(/(\d+) insertions?/) || [0, 0])[1];
const deletions = (diff.match(/(\d+) deletions?/) || [0, 0])[1];
return `update ${mainFile} (+${additions}/-${deletions})`;
}案例 3:Kubernetes Pod 诊断工具
# .opencode/tools/pod-debug.yaml
name: pod-debug
description: 诊断 Kubernetes Pod 状态并收集日志
parameters:
namespace:
type: string
description: "命名空间"
default: "default"
pod:
type: string
description: "Pod 名称(支持模糊匹配)"
container:
type: string
description: "容器名称(多容器 Pod)"
required: false
command: bash
args:
- -c
- |
NAMESPACE="{{namespace}}"
POD_PATTERN="{{pod}}"
CONTAINER="{{container}}"
# 模糊查找 Pod
POD_NAME=$(kubectl get pods -n "$NAMESPACE" --no-headers | grep "$POD_PATTERN" | head -1 | awk '{print $1}')
if [ -z "$POD_NAME" ]; then
echo '{"error": "未找到匹配的 Pod"}'
exit 1
fi
# 收集信息
DESCRIBE=$(kubectl describe pod "$POD_NAME" -n "$NAMESPACE" 2>&1)
EVENTS=$(echo "$DESCRIBE" | grep -A 20 "Events:")
RESTART_COUNT=$(echo "$DESCRIBE" | grep "Restart Count:" | awk '{print $3}')
# 收集日志
LOG_OPTS=""
[ -n "$CONTAINER" ] && LOG_OPTS="-c $CONTAINER"
LOGS=$(kubectl logs "$POD_NAME" -n "$NAMESPACE" $LOG_OPTS --tail=100 2>&1 || echo "无法获取日志")
# 输出结构化结果
cat <<EOF
{
"pod": "$POD_NAME",
"namespace": "$NAMESPACE",
"restartCount": $RESTART_COUNT,
"events": $(echo "$EVENTS" | jq -R -s '.'),
"logs": $(echo "$LOGS" | jq -R -s '.'),
"recommendations": [
$(if [ "$RESTART_COUNT" -gt 5 ]; then echo '"Pod 频繁重启,检查资源限制或探针配置"'; fi)
]
}
EOF
timeout: 30000工具类型对比表
| 特性 | Script Tool | HTTP Tool | Function Tool |
|---|---|---|---|
| 复杂度 | ⭐ 低 | ⭐⭐ 中 | ⭐⭐⭐ 高 |
| 灵活性 | 受限于命令行 | 受限于 API | 完全可控 |
| 调试难度 | 易(直接运行脚本) | 中(用 curl 模拟) | 需 TypeScript 环境 |
| 错误处理 | 依赖脚本输出 | 依赖 HTTP 状态码 | 可精细控制 |
| 性能 | 启动开销大 | 网络延迟 | 最快(内存中执行) |
| 适用场景 | 一次性命令、系统运维 | 第三方 API 集成 | 复杂业务逻辑 |
| 沙箱安全 | 进程隔离 | 网络隔离 | 代码级控制 |
FAQ
Q: AI 如何选择使用哪个工具?
A: OpenCode 根据工具 description 和当前对话上下文,通过 LLM 的 function calling 能力自动决策。描述越清晰,选择越准确。
Q: 工具返回的数据会占用 Token 吗? A: 是的,工具返回的完整结果会加入对话上下文。建议:
- 限制返回数据量(
max_results、pageSize) - 返回摘要而非原始数据
- 对大型结果提供分页链接
Q: 工具可以调用其他工具吗? A: 不可以。工具应是无状态的独立单元。如需组合能力,在 AI 层协调多个工具调用。
Q: 如何调试工具不生效的问题? A: 按以下顺序排查:
opencode --show-config确认工具已注册- 手动运行命令验证脚本本身无错
- 检查 JSON Schema 语法(可用 jsonlint)
- 查看
~/.local/share/opencode/logs/中的工具调用日志
避坑清单
| ❌ 错误做法 | ✅ 正确做法 | 原因 |
|---|---|---|
| 在工具中硬编码密钥 | 使用环境变量 ${API_KEY} | 防止密钥泄露到版本控制 |
| 返回无格式长文本 | 返回结构化 JSON | AI 需要结构化数据理解结果 |
| 忽略超时设置 | 为网络请求设置合理 timeout | 防止 AI 会话长时间阻塞 |
| 工具名过于笼统 | 使用语义化名称如 jira-create-task | 帮助 AI 准确选择工具 |
| 不写 description | 提供详细、准确的描述 | description 是 AI 选工具的依据 |
| 破坏性操作不设权限 | 部署/删除类工具设为 ask | 防止意外操作 |
| 参数无类型限制 | 使用 JSON Schema 严格约束 | 减少参数错误导致的失败 |
| 忽略错误返回 | 始终返回 {success, error} 结构 | 让 AI 能诊断并重试 |
工具开发最佳实践
DO ✅
- 返回结构化数据(JSON)
- 提供清晰的 description
- 处理边界情况和错误
- 设置合理的执行超时
- 使用语义化命名
- 做好权限分级
DON'T ❌
- 返回过多数据(占用上下文窗口)
- 执行破坏性操作不确认
- 硬编码敏感信息
- 忽略网络超时
- 提供模糊的工具描述
下一篇:17. 插件系统