docs: 修正 plugindev 工具链描述 + 补充入口函数/Lua 插件说明 + 补全示例插件列表

This commit is contained in:
JianFeeeee
2026-07-28 21:56:54 +08:00
parent 04837f237d
commit c7c66b8d39
8 changed files with 169 additions and 91 deletions

View File

@ -117,7 +117,7 @@ Triple 数据结构新增字段:
func New(name string, sett SettingsAPI, regTool ToolRegistrar, regStage StageRegistrar, regAPI APIRegistrar, regOutput OutputChannelRegistrar) *PluginSDK func New(name string, sett SettingsAPI, regTool ToolRegistrar, regStage StageRegistrar, regAPI APIRegistrar, regOutput OutputChannelRegistrar) *PluginSDK
``` ```
插件开发者只需实现 `Plugin` 接口并导出 `NewPlugin()` 入口函数。 插件开发者只需实现 `Plugin` 接口并导出 `NewPluginFactory()` 入口函数。
## plugindev 工具链 ## plugindev 工具链
@ -125,13 +125,25 @@ func New(name string, sett SettingsAPI, regTool ToolRegistrar, regStage StageReg
| 命令 | 说明 | | 命令 | 说明 |
|------|------| |------|------|
| `plugindev init` | 初始化插件项目(生成 plg.json、入口模板) | | `plugindev init <name> [--lua]` | 初始化插件项目(生成 plg.json、plugin.go 或 main.lua、go.mod、README.md) |
| `plugindev build` | 构建插件,输出 .hmap 包 | | `plugindev build [flags]` | 编译并打包为 `.hmap` 包(支持跨平台编译和 bundle 模式) |
| `plugindev clean` | 清理构建产物 | | `plugindev clean` | 清理 `build/`、`dist/` 目录及生成文件(plugin.json、z_bridge_gen.go) |
| `plugindev debug` | 本地调试模式运行插件 | | `plugindev debug [dir]` | 通过 Yaegi Go 解释器加载插件源码,启动交互式 REPL 调试 |
| `plugindev sdk <command>` | SDK 版本管理(子命令:list/install/use/path/current/latest) |
支持 **Go** 和 **Lua** 两种插件语言。 支持 **Go** 和 **Lua** 两种插件语言。
### build 命令 flags
| Flag | 说明 |
|------|------|
| `--outdir <dir>` | 输出目录(默认 `dist`,可覆盖 plg.json 中的 `outdir`) |
| `--target <os/arch>` | 构建目标(如 `linux/amd64`),可重复指定(追加到 plg.json 中的 targets) |
| `--bundle` | 强制 bundle 模式(同时编译 linux/amd64, darwin/amd64, windows/amd64) |
| `--no-bundle` | 关闭 bundle 模式,仅按 targets 逐个编译 |
| `--sdk-path <path>` | 指定 SDK 源码路径(覆盖 plg.json 中的 `sdk_path`) |
| `--replace <from=to>` / `-R` | Go 模块替换(追加到 plg.json 中的 replaces),`from` 为模块路径,`to` 为本地路径 |
### plg.json 清单格式 ### plg.json 清单格式
```json ```json
@ -164,13 +176,15 @@ func New(name string, sett SettingsAPI, regTool ToolRegistrar, regStage StageReg
| `version` | string | 版本号 | | `version` | string | 版本号 |
| `description` | string | 插件描述 | | `description` | string | 插件描述 |
| `author` | string | 作者 | | `author` | string | 作者 |
| `entry` | string | 入口文件(`plugin.so` / `main.lua`) | | `entry` | string | 入口文件(`plugin.so` / `plugin.dll` / `main.lua`) |
| `tags` | string[] | 标签 | | `tags` | string[] | 标签 |
| `targets` | string | 构建目标,逗号分隔(如 `linux/amd64,windows/amd64`) | | `targets` | string | 构建目标,逗号分隔(如 `linux/amd64,windows/amd64`,Lua 插件为 `lua`) |
| `outdir` | string | 输出目录(默认 `dist`) | | `outdir` | string | 输出目录(默认 `dist`) |
| `bundle` | bool | 是否 bundle 模式(同时编译多平台) | | `bundle` | bool | 是否 bundle 模式(同时编译多平台,默认 `true`) |
| `sdk_path` | string | SDK 源码路径(覆盖自动检测的 SDK 路径) |
| `go_version` | string | Go 版本(如 `1.21`,默认从 SDK 的 go.mod 读取) |
| `replaces` | object | Go 模块替换,key=模块路径,value=本地路径 | | `replaces` | object | Go 模块替换,key=模块路径,value=本地路径 |
| `source_dirs` | string[] | 额外源码搜索路径(编译时自动导入) | | `source_dirs` | string[] | 额外源码搜索路径(编译时自动导入,用于引入 `thirdpart/` 外部的共享代码) |
### .hmap 包格式 ### .hmap 包格式
@ -179,10 +193,34 @@ func New(name string, sett SettingsAPI, regTool ToolRegistrar, regStage StageReg
- `plugin.json` — 插件元数据 - `plugin.json` — 插件元数据
- `plugin.so` — Go 编译产物(Linux) - `plugin.so` — Go 编译产物(Linux)
- `plugin.dll` — Go 编译产物(Windows) - `plugin.dll` — Go 编译产物(Windows)
- `plugin.dylib` — Go 编译产物(macOS,bundle 模式)
- `main.lua` — Lua 插件入口(Lua 插件时) - `main.lua` — Lua 插件入口(Lua 插件时)
## 插件生命周期 ## 插件生命周期
### 入口函数
插件必须导出 `NewPluginFactory` 入口函数(Go)或 `start()` 函数(Lua):
**Go 插件** — 实现 `Plugin` 接口并导出工厂函数:
```go
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil
}
```
该函数由内核在加载插件时调用,`name` 为插件名,`config` 为 `skill.json` 中的配置(如有)。
**Lua 插件** — 返回包含 `start(sdk)` 和 `stop()` 方法的 table:
```lua
local plugin = { name = "my-plugin" }
function plugin.start(sdk) -- 注册工具等 end
function plugin.stop() end
return plugin
```
### 启动与停止 ### 启动与停止
- `Start(sdk *PluginSDK) error` — 插件启动,接收 SDK 实例 - `Start(sdk *PluginSDK) error` — 插件启动,接收 SDK 实例
@ -214,14 +252,19 @@ enabled := sdk.AutoRestart()
| 插件 | 说明 | | 插件 | 说明 |
|------|------| |------|------|
| a2a | Agent-to-Agent 协议通信 | | a2a | Agent-to-Agent 协议通信 |
| ai_image | AI 图片生成 |
| bili | Bilibili 视频下载 | | bili | Bilibili 视频下载 |
| browser | 网络搜索、网页抓取、浏览器渲染(合并自 web/webfetch) | | browser | 网络搜索、网页抓取、浏览器渲染 |
| calendar | 日历管理 |
| editdoc | 文档编辑 | | editdoc | 文档编辑 |
| files | 文件管理 | | files | 文件管理 |
| memo | 备忘录/记忆 | | memo | 备忘录 |
| music | 音乐播放 |
| ocr | 光学字符识别 | | ocr | 光学字符识别 |
| qq | QQ 消息集成 | | qq | QQ 消息集成(NapCat webhook,15 个工具) |
| rss | RSS 订阅 |
| sanitizer | 内容清洗/安全过滤 | | sanitizer | 内容清洗/安全过滤 |
| weather | 天气查询(wttr.in) |
## 构建与安装 ## 构建与安装

2
go.mod
View File

@ -1,3 +1,3 @@
module gitcode.com/JianFeeeee/homeagent-sdk module gitcode.com/JianFeeeee/homeagent-sdk
go 1.25.0 go 1.21.0

View File

@ -284,22 +284,29 @@ func ensureGoMod(plg *PlgConfig, sdkPath string) {
return return
} }
// 检查是否已有 replace 指令
absSDK, _ := filepath.Abs(sdkPath) absSDK, _ := filepath.Abs(sdkPath)
absSDK = strings.ReplaceAll(absSDK, "\\", "/") absSDK = strings.ReplaceAll(absSDK, "\\", "/")
// Remove any existing replace line for this module (even if path differs)
var keep []string
replaceLine := fmt.Sprintf("replace %s => %s", sdkModule, absSDK)
alreadyExists := false
for _, line := range lines { for _, line := range lines {
if strings.Contains(line, "replace") && strings.Contains(line, sdkModule) { if strings.HasPrefix(strings.TrimSpace(line), "replace ") &&
strings.Contains(line, sdkModule) {
parts := strings.Fields(line) parts := strings.Fields(line)
if len(parts) >= 3 && strings.ReplaceAll(parts[2], "\\", "/") == absSDK { if len(parts) >= 3 && strings.ReplaceAll(parts[2], "\\", "/") == absSDK {
return // 已存在且路径正确 alreadyExists = true
} }
continue // strip any existing replace for this module
} }
keep = append(keep, line)
} }
if alreadyExists {
// 追加 replace 指令 return
replaceLine := fmt.Sprintf("replace %s => %s", sdkModule, absSDK) }
newData := string(data) + "\n" + replaceLine + "\n" keep = append(keep, replaceLine, "")
if err := os.WriteFile(gomodPath, []byte(newData), 0644); err != nil { if err := os.WriteFile(gomodPath, []byte(strings.Join(keep, "\n")), 0644); err != nil {
fmt.Printf(" warn: update go.mod replace: %v\n", err) fmt.Printf(" warn: update go.mod replace: %v\n", err)
} }
} }
@ -387,7 +394,7 @@ func buildTarget(plg *PlgConfig, target, outDir, sdkPath string) {
// Auto-generate C ABI bridge (all platforms use c-shared) // Auto-generate C ABI bridge (all platforms use c-shared)
bridgeCleanup := generateBridge(cfg.goos) bridgeCleanup := generateBridge(cfg.goos)
_ = bridgeCleanup // DISABLED cleanup for debug defer bridgeCleanup()
// Auto-link thirdpart/ contents + source_dirs + replace targets // Auto-link thirdpart/ contents + source_dirs + replace targets
thirdpartCleanup := linkThirdpart(plg, target) thirdpartCleanup := linkThirdpart(plg, target)
@ -566,33 +573,6 @@ func detectWindowsCC() string {
// stripIncludeGuard strips preprocessor guards and C++ comments from a C header, // stripIncludeGuard strips preprocessor guards and C++ comments from a C header,
// since these can confuse cgo's type resolution. // since these can confuse cgo's type resolution.
func stripIncludeGuard(header string) string {
lines := strings.Split(header, "\n")
var out []string
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "#ifndef HOMEAGENT_CABI_H" || trimmed == "#define HOMEAGENT_CABI_H" {
continue
}
if trimmed == "#endif" || strings.HasPrefix(trimmed, "#endif") {
continue
}
if trimmed == "#ifdef __cplusplus" || trimmed == "extern \"C\" {" || trimmed == "}" {
continue
}
// Strip C++-style comments (cgo parser may not handle them in /* */ blocks)
if idx := strings.Index(line, "//"); idx >= 0 {
line = line[:idx]
}
cleaned := strings.TrimSpace(line)
if cleaned == "" {
continue
}
out = append(out, line)
}
return strings.Join(out, "\n")
}
// generateBridge generates the C ABI bridge files for non-Lua builds. // generateBridge generates the C ABI bridge files for non-Lua builds.
// Returns a cleanup function to remove generated files. // Returns a cleanup function to remove generated files.
func generateBridge(goos string) func() { func generateBridge(goos string) func() {
@ -662,7 +642,11 @@ func linkThirdpart(plg *PlgConfig, target string) func() {
dirs = append(dirs, "thirdpart") dirs = append(dirs, "thirdpart")
} }
dirs = append(dirs, plg.SourceDirs...) dirs = append(dirs, plg.SourceDirs...)
for _, to := range plg.Replaces { for _, r := range plg.ReplacesToSlice() {
_, to, found := strings.Cut(r, "=")
if !found {
continue
}
if abs, err := filepath.Abs(to); err == nil { if abs, err := filepath.Abs(to); err == nil {
if info, err := os.Stat(abs); err == nil && info.IsDir() { if info, err := os.Stat(abs); err == nil && info.IsDir() {
dirs = append(dirs, abs) dirs = append(dirs, abs)
@ -699,25 +683,21 @@ func linkThirdpart(plg *PlgConfig, target string) func() {
continue continue
} }
// Determine import path: for relative dirs under module, use module path prefix;
// for absolute paths, derive from replace or use package name
dirName := filepath.Base(d)
if !filepath.IsAbs(d) { if !filepath.IsAbs(d) {
importPath := modulePath + "/" + d importPath := modulePath + "/" + d
stubs = append(stubs, importPath) stubs = append(stubs, importPath)
} else { } else {
// External directory: use the replace "from" key if found, else use dir name // External directory: must be in replaces to get a valid import path
found := false for _, r := range plg.ReplacesToSlice() {
for from, to := range plg.Replaces { from, to, found := strings.Cut(r, "=")
if !found {
continue
}
if absTo, _ := filepath.Abs(to); absTo == d { if absTo, _ := filepath.Abs(to); absTo == d {
stubs = append(stubs, from) stubs = append(stubs, strings.TrimSpace(from))
found = true
break break
} }
} }
if !found && dirName != "" {
stubs = append(stubs, modulePath+"/"+dirName)
}
} }
} }

View File

@ -11,6 +11,7 @@ import (
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
"time"
) )
const sdkDirName = "plugindev/sdk" const sdkDirName = "plugindev/sdk"
@ -165,7 +166,8 @@ func cmdSDKInstall(version string) {
tmpPath := tmpFile.Name() tmpPath := tmpFile.Name()
defer os.Remove(tmpPath) defer os.Remove(tmpPath)
resp, err := http.Get(url) client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Get(url)
if err != nil { if err != nil {
fmt.Printf("error: download SDK %s: %v\n", version, err) fmt.Printf("error: download SDK %s: %v\n", version, err)
os.Exit(1) os.Exit(1)
@ -191,12 +193,19 @@ func cmdSDKInstall(version string) {
} }
defer os.RemoveAll(tmpDir) defer os.RemoveAll(tmpDir)
gzr, err := gzip.NewReader(openFile(tmpPath)) f, err := openFile(tmpPath)
if err != nil { if err != nil {
fmt.Printf("error: open archive: %v\n", err)
os.Exit(1)
}
gzr, err := gzip.NewReader(f)
if err != nil {
f.Close()
fmt.Printf("error: read archive: %v\n", err) fmt.Printf("error: read archive: %v\n", err)
os.Exit(1) os.Exit(1)
} }
defer gzr.Close() defer gzr.Close()
defer f.Close()
tr := tar.NewReader(gzr) tr := tar.NewReader(gzr)
for { for {
@ -241,8 +250,12 @@ func cmdSDKInstall(version string) {
gzr.Close() gzr.Close()
if err := os.Rename(tmpDir, dest); err != nil { if err := os.Rename(tmpDir, dest); err != nil {
fmt.Printf("error: move SDK to store: %v\n", err) // Cross-filesystem rename fallback
os.Exit(1) if err := copyDir(tmpDir, dest); err != nil {
fmt.Printf("error: move SDK to store: %v\n", err)
os.Exit(1)
}
os.RemoveAll(tmpDir)
} }
fmt.Printf("SDK version %s installed at %s\n", version, dest) fmt.Printf("SDK version %s installed at %s\n", version, dest)
@ -253,12 +266,12 @@ func cmdSDKInstall(version string) {
} }
} }
func openFile(path string) *os.File { func openFile(path string) (*os.File, error) {
f, err := os.Open(path) f, err := os.Open(path)
if err != nil { if err != nil {
panic(err) return nil, err
} }
return f return f, nil
} }
// cmdSDKUse switches the active SDK version. // cmdSDKUse switches the active SDK version.
@ -396,17 +409,19 @@ func compareSemver(a, b string) int {
return 0 return 0
} }
// parseSemver extracts [major, minor, patch] from a vX.Y.Z string. // parseSemver extracts [major, minor, patch] from a vX.Y.Z[-pre] string.
// Prerelease tags parse to the same major.minor.patch as their release (ignoring prerelease).
func parseSemver(tag string) [3]int { func parseSemver(tag string) [3]int {
var v [3]int var v [3]int
s := strings.TrimPrefix(tag, "v") s := strings.TrimPrefix(tag, "v")
// Strip prerelease suffix (-...)
if idx := strings.IndexByte(s, '-'); idx >= 0 {
s = s[:idx]
}
parts := strings.SplitN(s, ".", 3) parts := strings.SplitN(s, ".", 3)
for i, p := range parts { for i := 0; i < 3 && i < len(parts); i++ {
if i >= 3 {
break
}
n := 0 n := 0
fmt.Sscanf(p, "%d", &n) fmt.Sscanf(parts[i], "%d", &n)
v[i] = n v[i] = n
} }
return v return v
@ -433,6 +448,35 @@ func activeSDKRoot() string {
return root return root
} }
// copyDir recursively copies src to dst (cross-filesystem rename fallback).
func copyDir(src, dst string) error {
if err := os.MkdirAll(dst, 0755); err != nil {
return err
}
entries, err := os.ReadDir(src)
if err != nil {
return err
}
for _, e := range entries {
srcPath := filepath.Join(src, e.Name())
dstPath := filepath.Join(dst, e.Name())
if e.IsDir() {
if err := copyDir(srcPath, dstPath); err != nil {
return err
}
} else {
data, err := os.ReadFile(srcPath)
if err != nil {
return err
}
if err := os.WriteFile(dstPath, data, 0644); err != nil {
return err
}
}
}
return nil
}
// readMetaVersion reads the Version string from the SDK's meta/meta.go. // readMetaVersion reads the Version string from the SDK's meta/meta.go.
// If the file is missing or unreadable, returns "0.0.0". // If the file is missing or unreadable, returns "0.0.0".
func readMetaVersion(sdkRoot string) string { func readMetaVersion(sdkRoot string) string {

View File

@ -1,6 +1,6 @@
module github.com/JianFeeeee/homeagent-sdk/tools/plugindev module github.com/JianFeeeee/homeagent-sdk/tools/plugindev
go 1.25.0 go 1.21.0
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0 require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0

View File

@ -30,7 +30,7 @@ func (p *GoModPatcher) Apply() (func(), error) {
p.backup = string(data) p.backup = string(data)
var sb strings.Builder var sb strings.Builder
sb.WriteString(strings.TrimRight(string(data), "\n")) sb.WriteString(strings.TrimRight(string(data), "\r\n"))
sb.WriteString("\n") sb.WriteString("\n")
for _, r := range p.replaces { for _, r := range p.replaces {
from, to, found := strings.Cut(r, "=") from, to, found := strings.Cut(r, "=")

View File

@ -390,6 +390,7 @@ var (
coreAPI unsafe.Pointer coreAPI unsafe.Pointer
handlerMu sync.RWMutex handlerMu sync.RWMutex
coreAPIMu sync.RWMutex
toolHandlers = map[string]sdk.ToolHandler{} toolHandlers = map[string]sdk.ToolHandler{}
stageHandlers = map[string]sdk.StageHandler{} stageHandlers = map[string]sdk.StageHandler{}
outputHandlers = map[string]sdk.ToolHandler{} outputHandlers = map[string]sdk.ToolHandler{}
@ -398,29 +399,35 @@ var (
// ---- CoreAPI dispatch helpers ---- // ---- CoreAPI dispatch helpers ----
func callVoid(methodID int, s1, s2, s3 string, i1, i2 int) error { func callVoid(methodID int, s1, s2, s3 string, i1, i2 int) error {
coreAPIMu.RLock()
api := coreAPI
coreAPIMu.RUnlock()
var c1, c2, c3 *C.char var c1, c2, c3 *C.char
if s1 != "" { c1 = C.CString(s1); defer C.free(unsafe.Pointer(c1)) } if s1 != "" { c1 = C.CString(s1); defer C.free(unsafe.Pointer(c1)) }
if s2 != "" { c2 = C.CString(s2); defer C.free(unsafe.Pointer(c2)) } if s2 != "" { c2 = C.CString(s2); defer C.free(unsafe.Pointer(c2)) }
if s3 != "" { c3 = C.CString(s3); defer C.free(unsafe.Pointer(c3)) } if s3 != "" { c3 = C.CString(s3); defer C.free(unsafe.Pointer(c3)) }
var cErr *C.char var cErr *C.char
if C.ha_dispatch(C.int(methodID), coreAPI, c1, c2, c3, C.int(i1), C.int(i2), nil, &cErr) != 0 && cErr != nil { if C.ha_dispatch(C.int(methodID), api, c1, c2, c3, C.int(i1), C.int(i2), nil, &cErr) != 0 && cErr != nil {
return fmt.Errorf("%s", C.GoString(cErr)) return fmt.Errorf("%s", C.GoString(cErr))
} }
return nil return nil
} }
func callString(methodID int, s1, s2, s3 string, i1, i2 int) (string, error) { func callString(methodID int, s1, s2, s3 string, i1, i2 int) (string, error) {
coreAPIMu.RLock()
api := coreAPI
coreAPIMu.RUnlock()
var c1, c2, c3 *C.char var c1, c2, c3 *C.char
if s1 != "" { c1 = C.CString(s1); defer C.free(unsafe.Pointer(c1)) } if s1 != "" { c1 = C.CString(s1); defer C.free(unsafe.Pointer(c1)) }
if s2 != "" { c2 = C.CString(s2); defer C.free(unsafe.Pointer(c2)) } if s2 != "" { c2 = C.CString(s2); defer C.free(unsafe.Pointer(c2)) }
if s3 != "" { c3 = C.CString(s3); defer C.free(unsafe.Pointer(c3)) } if s3 != "" { c3 = C.CString(s3); defer C.free(unsafe.Pointer(c3)) }
var strResult, cErr *C.char var strResult, cErr *C.char
if C.ha_dispatch(C.int(methodID), coreAPI, c1, c2, c3, C.int(i1), C.int(i2), &strResult, &cErr) != 0 && cErr != nil { if C.ha_dispatch(C.int(methodID), api, c1, c2, c3, C.int(i1), C.int(i2), &strResult, &cErr) != 0 && cErr != nil {
return "", fmt.Errorf("%s", C.GoString(cErr)) return "", fmt.Errorf("%s", C.GoString(cErr))
} }
if strResult != nil { if strResult != nil {
result := C.GoString(strResult) result := C.GoString(strResult)
C.ha_dispatch(C.int(25), coreAPI, strResult, nil, nil, 0, 0, nil, nil) C.ha_dispatch(C.int(25), api, strResult, nil, nil, 0, 0, nil, nil)
return result, nil return result, nil
} }
return "", nil return "", nil
@ -571,7 +578,9 @@ func go_init_plugin(name *C.char, configJSON *C.char, errorOut **C.char) C.int {
func go_start_plugin(coreAPIptr unsafe.Pointer, coreVersion C.int, errorOut **C.char) C.int { func go_start_plugin(coreAPIptr unsafe.Pointer, coreVersion C.int, errorOut **C.char) C.int {
mu.Lock() mu.Lock()
plg := currentPlg plg := currentPlg
coreAPIMu.Lock()
coreAPI = coreAPIptr coreAPI = coreAPIptr
coreAPIMu.Unlock()
mu.Unlock() mu.Unlock()
_ = coreVersion _ = coreVersion
if plg == nil { *errorOut = C.CString("not initialized"); return 1 } if plg == nil { *errorOut = C.CString("not initialized"); return 1 }
@ -585,7 +594,9 @@ func go_stop_plugin(errorOut **C.char) C.int {
mu.Lock() mu.Lock()
plg := currentPlg plg := currentPlg
currentPlg = nil currentPlg = nil
coreAPIMu.Lock()
coreAPI = nil coreAPI = nil
coreAPIMu.Unlock()
mu.Unlock() mu.Unlock()
if plg != nil { if plg != nil {
if err := plg.Stop(); err != nil { *errorOut = C.CString(err.Error()); return 1 } if err := plg.Stop(); err != nil { *errorOut = C.CString(err.Error()); return 1 }

View File

@ -345,23 +345,23 @@ func New(name string) *PluginSDK {
func (s *PluginSDK) RegisterTool(name string, def ToolDef, handler ToolHandler) { func (s *PluginSDK) RegisterTool(name string, def ToolDef, handler ToolHandler) {
logf("register_tool: %s", name) logf("register_tool: %s", name)
mu.Lock() s.mu.Lock()
defer mu.Unlock() defer s.mu.Unlock()
s.toolDefs[name] = def s.toolDefs[name] = def
s.toolHandlers[name] = handler s.toolHandlers[name] = handler
} }
func (s *PluginSDK) RegisterStage(stage Stage, handler StageHandler) { func (s *PluginSDK) RegisterStage(stage Stage, handler StageHandler) {
logf("register_stage: %s", string(stage)) logf("register_stage: %s", string(stage))
mu.Lock() s.mu.Lock()
defer mu.Unlock() defer s.mu.Unlock()
s.stageHandlers[string(stage)] = handler s.stageHandlers[string(stage)] = handler
} }
func (s *PluginSDK) RegisterOutputChannel(name string, caps int, desc string, handler ToolHandler) { func (s *PluginSDK) RegisterOutputChannel(name string, caps int, desc string, handler ToolHandler) {
logf("register_output_channel: %s", name) logf("register_output_channel: %s", name)
mu.Lock() s.mu.Lock()
defer mu.Unlock() defer s.mu.Unlock()
s.outChannels[name] = handler s.outChannels[name] = handler
} }
@ -370,9 +370,9 @@ func (s *PluginSDK) RegisterPluginAPI(name string) {
} }
func (s *PluginSDK) CallTool(name string, args map[string]interface{}) (interface{}, error) { func (s *PluginSDK) CallTool(name string, args map[string]interface{}) (interface{}, error) {
mu.Lock() s.mu.RLock()
handler, ok := s.toolHandlers[name] handler, ok := s.toolHandlers[name]
mu.Unlock() s.mu.RUnlock()
if !ok { if !ok {
return nil, fmt.Errorf("tool not found: %s", name) return nil, fmt.Errorf("tool not found: %s", name)
} }
@ -380,9 +380,9 @@ func (s *PluginSDK) CallTool(name string, args map[string]interface{}) (interfac
} }
func (s *PluginSDK) CallStage(stage string, ctx *StageContext) error { func (s *PluginSDK) CallStage(stage string, ctx *StageContext) error {
mu.Lock() s.mu.RLock()
handler, ok := s.stageHandlers[stage] handler, ok := s.stageHandlers[stage]
mu.Unlock() s.mu.RUnlock()
if !ok { if !ok {
return nil return nil
} }
@ -390,8 +390,8 @@ func (s *PluginSDK) CallStage(stage string, ctx *StageContext) error {
} }
func (s *PluginSDK) ListTools() []ToolDef { func (s *PluginSDK) ListTools() []ToolDef {
mu.Lock() s.mu.RLock()
defer mu.Unlock() defer s.mu.RUnlock()
defs := make([]ToolDef, 0, len(s.toolDefs)) defs := make([]ToolDef, 0, len(s.toolDefs))
for _, def := range s.toolDefs { for _, def := range s.toolDefs {
defs = append(defs, def) defs = append(defs, def)