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

自定义工具开发

当内置工具无法满足需求时,你可以编写自己的 Tool。OpenCode 的工具系统遵循 MCP (Model Context Protocol) 规范,允许 AI 在对话中安全地调用外部能力。

目标:为 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 ToolHTTP ToolFunction Tool
复杂度⭐ 低⭐⭐ 中⭐⭐⭐ 高
灵活性受限于命令行受限于 API完全可控
调试难度易(直接运行脚本)中(用 curl 模拟)需 TypeScript 环境
错误处理依赖脚本输出依赖 HTTP 状态码可精细控制
性能启动开销大网络延迟最快(内存中执行)
适用场景一次性命令、系统运维第三方 API 集成复杂业务逻辑
沙箱安全进程隔离网络隔离代码级控制

FAQ

Q: AI 如何选择使用哪个工具? A: OpenCode 根据工具 description 和当前对话上下文,通过 LLM 的 function calling 能力自动决策。描述越清晰,选择越准确。

Q: 工具返回的数据会占用 Token 吗? A: 是的,工具返回的完整结果会加入对话上下文。建议:

  • 限制返回数据量(max_resultspageSize
  • 返回摘要而非原始数据
  • 对大型结果提供分页链接

Q: 工具可以调用其他工具吗? A: 不可以。工具应是无状态的独立单元。如需组合能力,在 AI 层协调多个工具调用。

Q: 如何调试工具不生效的问题? A: 按以下顺序排查:

  1. opencode --show-config 确认工具已注册
  2. 手动运行命令验证脚本本身无错
  3. 检查 JSON Schema 语法(可用 jsonlint)
  4. 查看 ~/.local/share/opencode/logs/ 中的工具调用日志

避坑清单

❌ 错误做法✅ 正确做法原因
在工具中硬编码密钥使用环境变量 ${API_KEY}防止密钥泄露到版本控制
返回无格式长文本返回结构化 JSONAI 需要结构化数据理解结果
忽略超时设置为网络请求设置合理 timeout防止 AI 会话长时间阻塞
工具名过于笼统使用语义化名称如 jira-create-task帮助 AI 准确选择工具
不写 description提供详细、准确的描述description 是 AI 选工具的依据
破坏性操作不设权限部署/删除类工具设为 ask防止意外操作
参数无类型限制使用 JSON Schema 严格约束减少参数错误导致的失败
忽略错误返回始终返回 {success, error} 结构让 AI 能诊断并重试

工具开发最佳实践

DO ✅

  • 返回结构化数据(JSON)
  • 提供清晰的 description
  • 处理边界情况和错误
  • 设置合理的执行超时
  • 使用语义化命名
  • 做好权限分级

DON'T ❌

  • 返回过多数据(占用上下文窗口)
  • 执行破坏性操作不确认
  • 硬编码敏感信息
  • 忽略网络超时
  • 提供模糊的工具描述

下一篇:17. 插件系统

分享

评论

登录 后参与讨论。

加载中…

相关文章