第 10 章

外掛與自訂工具

學習如何透過外掛系統擴充 opencode 的功能,以及如何編寫自訂工具來滿足專案特定的需求。

高級 Advanced

學習目標

外掛系統概述

opencode 的外掛系統讓你可以在不修改核心程式碼的情況下,擴充或修改工具的行為。外掛可以監聽事件、注入環境變數、新增自訂工具,以及保護特定檔案不被意外修改。

外掛的載入位置

外掛可以來自兩種來源:

來源設定方式範例
本地檔案指定 .js.mjs 檔案的路徑"./plugins/my-plugin.js"
npm 套件安裝 npm 套件後直接引用"opencode-plugin-env"

外掛設定在 opencode.jsonplugins 陣列中:

{
  "plugins": [
    "./plugins/notifier.js",
    "opencode-plugin-env"
  ]
}

外掛也可以是物件形式,以便傳遞自訂選項:

{
  "plugins": [
    {
      "name": "./plugins/notifier.js",
      "options": {
        "sound": "chime",
        "notifyOn": ["tool:start", "tool:end"]
      }
    }
  ]
}

外掛基本結構

一個外掛是一個匯出一個或多個鉤子函式的 JavaScript 或 TypeScript 模組:

// plugins/notifier.js
export const name = "notifier";

export const hooks = {
  // 在工具執行前觸發
  "tool:start"({ tool, args }) {
    console.log(`[Plugin] 工具 ${tool} 開始執行`);
  },
  // 在工具執行完成後觸發
  "tool:end"({ tool, args, result }) {
    console.log(`[Plugin] 工具 ${tool} 執行完畢`);
  },
  // 在代理回覆前觸發
  "before:reply"({ message }) {
    console.log(`[Plugin] 代理準備回覆`);
  }
};

事件分類

外掛可監聽以下事件分類:

事件前綴說明
tool:start工具開始執行時
tool:end工具執行結束時
tool:error工具執行發生錯誤時
before:reply代理產生回覆前
after:reply代理回覆完成後
agent:start代理開始處理時
agent:end代理處理完成時

實用外掛範例

桌面通知外掛

當代理執行特定操作時,發送桌面通知:

// plugins/desktop-notifier.js
export const name = "desktop-notifier";

export const hooks = {
  "tool:start"({ tool, args }) {
    if (tool === "bash" || tool === "write") {
      // 使用作業系統的通知機制
      // 例如 Notification API(需在特定環境)
    }
  }
};

.env 檔案保護

防止代理意外修改敏感的環境變數檔:

// plugins/env-protector.js
export const name = "env-protector";

export const hooks = {
  "tool:start"({ tool, args }) {
    if (tool === "write" || tool === "edit") {
      const filePath = args.filePath || "";
      if (filePath.includes(".env") && !filePath.includes(".env.example")) {
        throw new Error("不允許直接修改 .env 檔案");
      }
    }
  }
};

運作原理:當鉤子函式拋出錯誤時,該工具執行會被中斷,代理會收到錯誤訊息。這個機制讓外掛能有效地控制工具的行為。

注入環境變數

可透過 npm 套件 opencode-plugin-env 自動從檔案注入環境變數:

{
  "plugins": [
    {
      "name": "opencode-plugin-env",
      "options": {
        "files": [".env.local", ".env.development"]
      }
    }
  ]
}

自訂工具外掛

外掛也可以註冊全新的工具:

// plugins/weather-tool.js
export const name = "weather";

export const tools = [
  {
    name: "get_weather",
    description: "取得指定城市的目前天氣",
    parameters: {
      type: "object",
      properties: {
        city: {
          type: "string",
          description: "城市名稱"
        }
      },
      required: ["city"]
    },
    async execute({ city }) {
      const res = await fetch(`https://api.weather.com/${city}`);
      return res.json();
    }
  }
];

自訂工具

除了透過外掛註冊工具,你也可以直接在專案中定義自訂工具,讓代理在該專案中可以使用。

位置與結構

自訂工具放在專案根目錄的 .opencode/tools/ 目錄下,每個工具是一個獨立的 .js.mjs 檔案:

.opencode/
  └── tools/
      ├── analyze-log.js
      └── deploy.js

opencode 會自動發現並載入此目錄下的所有工具檔案。

使用 tool() 輔助函式

opencode 提供 tool() 輔助函式,讓工具定義更簡潔:

// .opencode/tools/analyze-log.js
import { tool } from "opencode/tool";

export default tool({
  name: "analyze-log",
  description: "分析日誌檔案中的錯誤模式",
  parameters: {
    filePath: { type: "string", description: "日誌檔案路徑" },
    pattern: { type: "string", description: "搜尋模式(選填)", optional: true }
  },
  async execute({ filePath, pattern }) {
    const content = await readFile(filePath, "utf-8");
    const lines = content.split("\n");
    const errors = lines.filter(l => l.includes("ERROR"));
    return `找到 ${errors.length} 筆錯誤`;
  }
});

注意:工具檔案必須使用 ESM 格式(export default),並以 .js.mjs 為副檔名。不支援 CommonJS(module.exports)。

參數定義(Zod Schema)

自訂工具也支援使用 Zod 來定義參數結構,以獲得更好的型別檢查與驗證:

// .opencode/tools/deploy.js
import { tool } from "opencode/tool";
import { z } from "zod";

export default tool({
  name: "deploy",
  description: "部署專案到指定環境",
  parameters: z.object({
    environment: z
      .enum(["staging", "production"])
      .describe("部署目標環境"),
    branch: z
      .string()
      .default("main")
      .describe("要部署的分支名稱"),
    force: z
      .boolean()
      .optional()
      .describe("強制部署(跳過檢查)")
  }),
  async execute({ environment, branch, force }) {
    // 執行部署邏輯
    return `部署 ${branch} 到 ${environment} 成功`;
  }
});

上下文資訊

工具的 execute 函式可以接收第二個參數,包含工具執行時期的上下文資訊:

export default tool({
  name: "config-env",
  description: "讀取環境設定",
  parameters: { key: { type: "string" } },
  async execute({ key }, context) {
    // context 包含:
    // - context.projectDir: 專案根目錄
    // - context.config: opencode 設定內容
    // - context.args: 用戶輸入的原始參數
    // - context.signal: AbortSignal
    return process.env[key] || "未設定";
  }
});

用 Python 編寫自訂工具

opencode 也支援使用 Python 編寫工具。你需要安裝 opencode-sdk 套件,並使用 @opencode_tool 裝飾器:

# .opencode/tools/docker-status.py
from opencode_sdk import opencode_tool
import subprocess
import json

@opencode_tool(
    name="docker-status",
    description="檢查 Docker 容器狀態",
    parameters={
        "container_name": {
            "type": "string",
            "description": "容器名稱(選填,省略則列出全部)",
            "optional": True
        }
    }
)
def docker_status(container_name=None):
    cmd = ["docker", "ps", "--format", "{{.Names}}\t{{.Status}}"]
    if container_name:
        cmd.extend(["--filter", f"name={container_name}"])
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        return f"錯誤: {result.stderr}"
    if not result.stdout.strip():
        return "無執行中的容器"
    lines = [line.split("\t") for line in result.stdout.strip().split("\n")]
    return json.dumps(
        [{"name": n, "status": s} for n, s in lines],
        ensure_ascii=False, indent=2
    )

Python SDK 安裝:pip install opencode-sdk。支援 Python 3.10 以上版本。SDK 會自動處理工具的通訊協定,你只需要專注在工具邏輯本身。

實戰練習

練習 1:建立保護外掛

  1. 建立 plugins/protector.js 外掛檔案
  2. 監聽 tool:start 事件
  3. 當工具為 write 且目標檔案是 package.json 時拋出錯誤
  4. opencode.json 中註冊此外掛
  5. 啟動 opencode 並嘗試要求代理修改 package.json,觀察保護機制

練習 2:建立自訂工具

  1. .opencode/tools/ 目錄下建立 count-lines.js
  2. 工具接受 filePath 參數,回傳該檔案的總行數
  3. 使用 tool() 輔助函式和 Zod schema
  4. 啟動 opencode 並測試此工具是否被正確載入與執行

練習 3:Python 工具整合

  1. 安裝 Python SDK:pip install opencode-sdk
  2. 建立 .opencode/tools/file-stats.py 工具
  3. 實作功能:計算目錄中的檔案數量、總大小、最大檔案
  4. 確認 opencode 能自動載入並使用此 Python 工具