OpenCode 自定义工具(Custom Tools)开发实战:从零构建专属开发利器

引言

使用 OpenCode 进行 AI 辅助编程时,内置的工具集已经覆盖了文件读写、代码搜索、终端执行等常见场景。但在实际项目中,我们常常需要与内部 API、私有数据库、特定命令行工具或自定义工作流交互。这时,OpenCode 的 Custom Tools(自定义工具)功能就派上了用场。

自定义工具允许你定义 OpenCode Agent 可以调用的外部工具,从而扩展其能力边界。本文将深入讲解 Custom Tools 的配置方法、开发技巧和实战案例,帮助你将 OpenCode 打造成适配团队工作流的专属开发助手。

什么是 Custom Tools?

Custom Tools 是 OpenCode 中一种可扩展机制,允许你在 opencode.json 配置文件中定义新的工具,让 Agent 在对话中自动调用。每个自定义工具本质上是一个可执行命令的封装——Agent 决定何时调用、传入参数,然后获取命令的输出来辅助决策。

与 MCP(Model Context Protocol)服务器不同,Custom Tools 更轻量,无需启动独立服务进程,适合封装简单的脚本或命令行操作。

基础配置结构

自定义工具在 opencode.jsoncustomTools 字段中定义。来看一个最小示例:

{
  "customTools": {
    "weather": {
      "displayName": "天气查询",
      "description": "查询指定城市的当前天气信息",
      "command": "python scripts/weather.py"
    }
  }
}

每个工具的核心字段:

  • 键名(如上例的 weather):工具的唯一标识,Agent 通过它引用工具
  • displayName:显示名称,用于对话界面的展示
  • description:至关重要!Agent 靠描述来判断何时调用此工具,描述越清晰,Agent 选择越准确
  • command:要执行的命令,可以是脚本路径或内联命令

参数定义与传递

大多数工具需要接收参数。通过 args 字段定义参数结构,Agent 会理解这些参数并自动填充:

{
  "customTools": {
    "deploy": {
      "displayName": "部署服务",
      "description": "将指定分支部署到目标环境(staging/production)",
      "command": "bash scripts/deploy.sh",
      "args": {
        "branch": {
          "description": "要部署的分支名称",
          "required": true
        },
        "env": {
          "description": "目标环境:staging 或 production",
          "required": true,
          "enum": ["staging", "production"]
        },
        "force": {
          "description": "是否强制部署(跳过检查)",
          "required": false
        }
      }
    }
  }
}

参数通过命令行位置或环境变量传递给脚本。OpenCode 会按参数定义的顺序拼接命令。

更灵活的做法是通过 stdin 或文件传递参数,在脚本内部解析:

{
  "customTools": {
    "db_query": {
      "displayName": "数据库查询",
      "description": "对项目数据库执行只读 SQL 查询并返回结果",
      "command": "python scripts/query_db.py",
      "args": {
        "sql": {
          "description": "要执行的 SQL 查询语句",
          "required": true
        }
      }
    }
  }
}

对应的 Python 脚本 scripts/query_db.py

import sys
import json
import sqlite3

input_data = json.loads(sys.stdin.read())
sql = input_data.get("sql", "")

conn = sqlite3.connect("database.sqlite")
cursor = conn.cursor()
cursor.execute(sql)
rows = cursor.fetchall()
columns = [desc[0] for desc in cursor.description]

result = {"columns": columns, "rows": rows}
print(json.dumps(result, ensure_ascii=False))
conn.close()

实战案例一:Git 工作流自动化

在团队协作中,经常需要执行 Git 操作。创建一个自定义工具来简化分支合并和发布流程:

{
  "customTools": {
    "git_merge_check": {
      "displayName": "合并冲突检查",
      "description": "检查两个分支之间是否存在合并冲突,返回冲突文件列表",
      "command": "bash .opencode/scripts/merge_check.sh",
      "args": {
        "source": {
          "description": "源分支名称",
          "required": true
        },
        "target": {
          "description": "目标分支名称",
          "required": true
        }
      }
    }
  }
}

对应的 Shell 脚本 .opencode/scripts/merge_check.sh

#!/bin/bash
source=$1
target=$2

git fetch origin "$source" "$target" 2>/dev/null

if git merge-base --is-ancestor "origin/$source" "origin/$target"; then
  echo "无冲突:$source 已完全包含在 $target 中"
  exit 0
fi

conflicts=$(git merge-tree $(git merge-base "origin/$source" "origin/$target") "origin/$source" "origin/$target" | grep -A 3 "^changed in both" || true)

if [ -z "$conflicts" ]; then
  echo "无冲突:两个分支可以自动合并"
else
  echo "发现潜在冲突:"
  echo "$conflicts"
fi

Agent 可以在执行合并操作前自动调用此工具检查冲突风险,极大减少意外合并事故。

实战案例二:数据库 Schema 变更追踪

在 Laravel 或 Rails 项目中,数据库迁移是一个日常操作。创建一个工具来预览未执行的迁移及其影响:

{
  "customTools": {
    "pending_migrations": {
      "displayName": "待执行迁移",
      "description": "列出项目中所有未执行的数据库迁移文件及其内容摘要",
      "command": "php artisan migrate:status --json",
      "args": {}
    },
    "migration_preview": {
      "displayName": "迁移预览",
      "description": "预览指定迁移文件的 SQL 变更内容",
      "command": "bash .opencode/scripts/preview_migration.sh",
      "args": {
        "migration": {
          "description": "迁移文件名(如 2026_01_01_000001_create_users_table)",
          "required": true
        }
      }
    }
  }
}

实战案例三:调用内部 API

企业中常用内部 API 管理服务状态。以下工具封装了调用内部部署平台 API 的操作:

{
  "customTools": {
    "internal_api": {
      "displayName": "内部 API 调用",
      "description": "调用公司内部管理 API,获取服务状态或执行操作。支持 GET/POST 方法。",
      "command": "python .opencode/scripts/internal_api.py",
      "args": {
        "endpoint": {
          "description": "API 路径,例如 /api/services/status",
          "required": true
        },
        "method": {
          "description": "HTTP 方法",
          "required": false,
          "default": "GET",
          "enum": ["GET", "POST"]
        },
        "data": {
          "description": "POST 请求的 JSON 数据体",
          "required": false
        }
      }
    }
  }
}

Python 脚本 .opencode/scripts/internal_api.py

import sys, json, os, requests

API_BASE = os.environ.get("INTERNAL_API_URL", "http://localhost:8080")
API_TOKEN = os.environ.get("INTERNAL_API_TOKEN", "")

input_data = json.loads(sys.stdin.read())
endpoint = input_data["endpoint"]
method = input_data.get("method", "GET").upper()
data = input_data.get("data")

headers = {"Authorization": f"Bearer {API_TOKEN}", "Content-Type": "application/json"}

try:
    if method == "GET":
        resp = requests.get(f"{API_BASE}{endpoint}", headers=headers, timeout=10)
    else:
        payload = json.loads(data) if isinstance(data, str) else data
        resp = requests.post(f"{API_BASE}{endpoint}", headers=headers, json=payload, timeout=10)

    result = {
        "status_code": resp.status_code,
        "body": resp.json() if resp.text else {}
    }
except Exception as e:
    result = {"error": str(e)}

print(json.dumps(result, ensure_ascii=False))

通过在 Shell 中提前设置 INTERNAL_API_URLINTERNAL_API_TOKEN 环境变量,既能实现自动化,又避免在配置文件中明文存储密钥。

最佳实践与注意事项

1. 描述要精确

Agent 的决策完全依赖 description 字段。对比以下两个描述:

  • ❌ "执行一个脚本"
  • ✅ "查询 MySQL 数据库中指定表的记录数,返回整数结果。当用户询问数据量或表大小时使用。"

描述中应包含:工具的用途、输入输出格式、触发场景。

2. 脚本输出要结构化

Agent 解析的是标准输出,推荐使用 JSON 格式返回结构化数据,便于 Agent 提取关键信息。

print(json.dumps({"success": true, "message": "部署完成", "url": "https://staging.example.com"}))

3. 错误处理要周全

脚本应捕获所有异常,返回明确错误信息而非堆栈跟踪:

try:
    result = do_something()
    print(json.dumps({"success": True, "data": result}))
except Exception as e:
    print(json.dumps({"success": False, "error": str(e)}))
    sys.exit(1)

Agent 在遇到非零退出码时会认为工具执行失败,并尝试其他策略或向用户报告。

4. 命令执行路径

推荐将自定义脚本放在项目内的 .opencode/scripts/ 目录下,这样既与项目绑定,又不会污染根目录。使用相对于工作目录的路径:

"command": "bash .opencode/scripts/deploy.sh"

5. 安全性考虑

  • 避免在命令中包含敏感信息,使用环境变量传递密钥
  • 对参数做校验和过滤,防止命令注入
  • 执行数据库操作时限定只读查询
  • 使用 permissions 字段限制工具的调用范围

调试自定义工具

开发自定义工具时,可以先在终端手动测试:

echo '{"sql": "SELECT COUNT(*) FROM users"}' | python scripts/query_db.py

确认输出格式正确后,再集成到 OpenCode 中。在对话中使用时,Agent 调用工具的输入输出会显示在界面中,便于观察和调试。

总结

OpenCode 的 Custom Tools 机制将 AI Agent 的能力边界从通用编程扩展到了团队专属的业务场景。通过编写简单的脚本并定义清晰的参数结构,我们可以让 OpenCode 理解内部 API、执行复杂的 Git 操作、查询数据库、触发 CI/CD 流程,真正做到"一个 Agent 搞定开发全流程"。

本文从基础配置讲到了三个实战案例,涵盖 Git 自动化、数据库操作和内部 API 集成。关键要点是:精确的描述、结构化的输出、完善的错误处理,以及合理的安全实践。

当你下次遇到重复性操作时,不妨想一想:能否把它封装成一个 Custom Tool,让 OpenCode 来代劳?