feat: sdk NoMemory/Cleaner + example build fixes

- sdk/plugin.go: ToolDef adds NoMemory/Cleaner fields
- sdk/plugin_test.go: unit tests for NoMemory/Cleaner
- all example plugins: NoMemory/Cleaner annotated for each tool
- plugindev/templates.go: template shows NoMemory/Cleaner pattern
- plugindev/cmd_build.go: fix ensureGoMod, buildBundle/buildTarget sdkPath param, NewPluginFactory
- example/go.mod: add external dependency declarations (chromedp, gofeed)
- add main.go stubs for all example plugins
This commit is contained in:
JianFeeeee
2026-07-25 11:17:21 +08:00
parent 34f91ebd1a
commit cb7999ca71
54 changed files with 682 additions and 475 deletions

View File

@ -14,8 +14,9 @@ import (
type BuildConfig struct {
OutDir string
Targets []string // "linux/amd64", "windows/amd64", "lua"
Targets []string
Bundle bool
SDKPath string
}
func cmdBuild(args []string) {
@ -24,48 +25,43 @@ func cmdBuild(args []string) {
switch args[i] {
case "--outdir":
if i+1 < len(args) {
cfg.OutDir = args[i+1]
i++
cfg.OutDir = args[i+1]; i++
}
case "--target":
if i+1 < len(args) {
cfg.Targets = append(cfg.Targets, args[i+1])
i++
cfg.Targets = append(cfg.Targets, args[i+1]); i++
}
case "--bundle":
cfg.Bundle = true
case "--sdk-path":
if i+1 < len(args) {
cfg.SDKPath = args[i+1]; i++
}
}
}
// read plg.json
plg, err := readPlgJSON("plg.json")
if err != nil {
fmt.Printf("error: read plg.json: %v\n", err)
os.Exit(1)
fmt.Printf("error: read plg.json: %v\n", err); os.Exit(1)
}
if plg.IsLua() {
buildTarget(plg, "lua", cfg.OutDir)
buildTarget(plg, "lua", cfg.OutDir, "")
return
}
if cfg.Bundle {
buildBundle(plg, cfg.OutDir)
// Ensure go.mod exists with correct SDK path
ensureGoMod(plg, cfg.SDKPath)
// Default: bundle mode (all 3 platforms in one .hmap)
if cfg.Bundle || len(cfg.Targets) == 0 {
buildBundle(plg, cfg.OutDir, cfg.SDKPath)
return
}
// determine targets
targets := cfg.Targets
if len(targets) == 0 {
targets = parseTargets(plg.Targets)
}
if len(targets) == 0 {
targets = []string{"native"}
}
// build for each target
for _, t := range targets {
buildTarget(plg, t, cfg.OutDir)
// Explicit --target: build each separately
for _, t := range cfg.Targets {
buildTarget(plg, t, cfg.OutDir, cfg.SDKPath)
}
}
@ -80,7 +76,7 @@ var allBundleTargets = []struct {
{"windows/amd64", "plugin.dll"},
}
func buildBundle(plg *PlgConfig, outDir string) {
func buildBundle(plg *PlgConfig, outDir string, sdkPath string) {
os.MkdirAll(outDir, 0755)
buildDir := "build"
os.MkdirAll(buildDir, 0755)
@ -141,9 +137,7 @@ func buildBundle(plg *PlgConfig, outDir string) {
fmt.Printf(" packaged %s\n", filepath.Base(hmapPath))
}
func (p *PlgConfig) IsLua() bool {
return p.Entry == "main.lua"
}
func (p *PlgConfig) IsLua() bool { return p.Entry == "main.lua" }
func readPlgJSON(path string) (*PlgConfig, error) {
data, err := os.ReadFile(path)
@ -225,7 +219,108 @@ func resolveBuild(target string) (*buildConfig, string) {
}
}
func buildTarget(plg *PlgConfig, target, outDir string) {
// ensureGoMod 确保插件项目的 go.mod 包含 SDK 的 replace 指令。
// 如果 go.mod 不存在或已有正确 replace则跳过。
func ensureGoMod(plg *PlgConfig, sdkPath string) {
if sdkPath == "" {
// 从 plugindev 自身推断 SDK 路径
self, err := os.Executable()
if err != nil {
return
}
cand := filepath.Dir(filepath.Dir(filepath.Dir(self)))
if _, err := os.Stat(filepath.Join(cand, "sdk", "plugin.go")); err != nil {
return
}
sdkPath = cand
}
gomodPath := "go.mod"
data, err := os.ReadFile(gomodPath)
if err != nil {
return // no go.mod, skip
}
lines := strings.Split(string(data), "\n")
var sdkModule string
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "require ") || strings.HasPrefix(line, "require (") {
continue
}
if strings.Contains(line, "homeagent-sdk/sdk") || strings.Contains(line, "homeagent-sdk") {
parts := strings.Fields(line)
if len(parts) >= 1 && !strings.HasPrefix(parts[0], "//") && !strings.HasPrefix(parts[0], "replace") {
sdkModule = parts[0]
}
}
}
if sdkModule == "" {
return
}
// 检查是否已有 replace 指令
absSDK, _ := filepath.Abs(sdkPath)
absSDK = strings.ReplaceAll(absSDK, "\\", "/")
for _, line := range lines {
if strings.Contains(line, "replace") && strings.Contains(line, sdkModule) {
parts := strings.Fields(line)
if len(parts) >= 3 && strings.ReplaceAll(parts[2], "\\", "/") == absSDK {
return // 已存在且路径正确
}
}
}
// 追加 replace 指令
replaceLine := fmt.Sprintf("replace %s => %s", sdkModule, absSDK)
newData := string(data) + "\n" + replaceLine + "\n"
if err := os.WriteFile(gomodPath, []byte(newData), 0644); err != nil {
fmt.Printf(" warn: update go.mod replace: %v\n", err)
}
}
func resolveSDKPath(sdkPath string) string {
if sdkPath != "" {
abs, _ := filepath.Abs(sdkPath)
if _, err := os.Stat(filepath.Join(abs, "sdk", "plugin.go")); err == nil {
return abs
}
fmt.Printf("error: --sdk-path %q not a valid SDK\n", sdkPath)
os.Exit(1)
}
// Detect from plugindev's own location (internal dev)
self, err := os.Executable()
if err == nil {
cand := filepath.Dir(filepath.Dir(filepath.Dir(self)))
if _, err := os.Stat(filepath.Join(cand, "sdk", "plugin.go")); err == nil {
return cand
}
}
// Active SDK via plugindev sdk use
store := os.Getenv("HOMEAGENT_SDK_DIR")
if store == "" {
home, _ := os.UserHomeDir()
if home != "" {
store = filepath.Join(home, ".homeagent", "plugindev", "sdk")
}
}
if store != "" {
if d, err := os.ReadFile(filepath.Join(store, "current")); err == nil {
ver := strings.TrimSpace(string(d))
if ver != "" {
root := filepath.Join(store, ver)
if _, err := os.Stat(filepath.Join(root, "sdk", "plugin.go")); err == nil {
return root
}
}
}
}
fmt.Printf("error: cannot locate SDK. Use --sdk-path or 'plugindev sdk use'\n")
os.Exit(1)
return ""
}
func buildTarget(plg *PlgConfig, target, outDir, sdkPath string) {
os.MkdirAll(outDir, 0755)
// Lua: no compilation, package source directly

View File

@ -46,8 +46,14 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
})
tp := p.name + "_"
s.RegisterTool(tp+"hello", sdk.ToolDef{
Name: tp + "hello", Description: "A hello world tool",
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
Name: tp + "hello",
Description: "A hello world tool",
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
NoMemory: false, // 工具输出对 LLM 注意力有信号价值时为 false纯操作工具为 true
// Cleaner: func(output string) string {
// // 工具输出参与向量化/jieba/蒸馏前,在此过滤噪音
// return output
// },
}, p.handleHello)
fmt.Printf("[%s] started\n", p.name)
return nil
@ -59,7 +65,7 @@ func (p *Plugin) handleHello(args map[string]interface{}) (interface{}, error) {
return map[string]interface{}{"content": "Hello from {{.Plg.Name}} plugin!"}, nil
}
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil
}
`
@ -157,7 +163,7 @@ func NewPlugin(name *C.char, configJSON *C.char) unsafe.Pointer {
if c, ok := wrapper["config"].(map[string]interface{}); ok { config = c }
}
}
plg, err := NewPlugin(goName, config)
plg, err := NewPluginFactory(goName, config)
if err != nil { return nil }
return newHandle(plg)
}
@ -173,6 +179,7 @@ func StartPlugin(handle unsafe.Pointer) C.int {
},
func(stage sdk.Stage, handler sdk.StageHandler) { bs.stages[string(stage)] = handler },
func(name string) error { return nil },
func(name string, caps int, desc string, handler sdk.ToolHandler) error { return nil },
)
if err := bs.plugin.Start(mockSDK); err != nil { return 1 }
return 0
@ -207,11 +214,11 @@ func InvokeToolJSON(handle unsafe.Pointer, toolName *C.char, argsJSON *C.char) *
if bs == nil || toolName == nil { return nil }
goName := C.GoString(toolName)
handler, ok := bs.handlers[goName]
if !ok { r, _ := json.Marshal(map[string]interface{}{"error": "tool not found: " + goName}); return C.CString(string(r)) }
if !ok { errMsg, _ := json.Marshal(map[string]interface{}{"error": "tool not found: " + goName}); return C.CString(string(errMsg)) }
var args map[string]interface{}
if argsJSON != nil { json.Unmarshal([]byte(C.GoString(argsJSON)), &args) }
r, err := handler(args)
if err != nil { r, _ = json.Marshal(map[string]interface{}{"error": err.Error()}); return C.CString(string(r)) }
if err != nil { errMsg, _ := json.Marshal(map[string]interface{}{"error": err.Error()}); return C.CString(string(errMsg)) }
b, _ := json.Marshal(r)
return C.CString(string(b))
}

View File

@ -1 +0,0 @@
I need to rewrite the preamble section. Let me use a python script to make this change.

View File

@ -1,13 +0,0 @@
# testplugin
testplugin plugin
## Build
```bash
plugindev build
```
## Install
Upload the .hmap file through the Plugin Manager API.

View File

@ -1,7 +0,0 @@
module testplugin
go 1.21
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
replace gitcode.com/JianFeeeee/homeagent-sdk => E:/program/homeagent/homeagentsdk

View File

@ -1,11 +0,0 @@
//go:build !windows || !cgo
package main
import (
"gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
return NewPluginFactory(name, config)
}

View File

@ -1,11 +0,0 @@
{
"name": "testplugin",
"name_zh": "中文名",
"name_en": "Testplugin",
"version": "0.1.0",
"description": "testplugin plugin",
"author": "HomeAgent",
"entry": "plugin.so",
"tags": ["testplugin"],
"targets": "linux/amd64,windows/amd64"
}

View File

@ -1,57 +0,0 @@
package main
import (
"fmt"
"gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
type Plugin struct {
name string
sdk *sdk.PluginSDK
}
func (p *Plugin) Name() string { return p.name }
func (p *Plugin) Start(s *sdk.PluginSDK) error {
p.sdk = s
s.Settings().RegisterDef(sdk.ConfigDef{
Key: "plugin.testplugin.example",
Default: "hello",
Type: "string",
DisplayName: "示例配置",
Description: "An example configuration key",
Category: "testplugin",
})
tp := p.name + "_"
s.RegisterTool(tp+"hello", sdk.ToolDef{
Name: tp + "hello",
Description: "A hello world tool",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
},
}, p.handleHello)
fmt.Printf("[%s] started\n", p.name)
return nil
}
func (p *Plugin) Stop() error {
fmt.Printf("[%s] stopped\n", p.name)
return nil
}
func (p *Plugin) handleHello(args map[string]interface{}) (interface{}, error) {
return map[string]interface{}{
"content": "Hello from testplugin plugin!",
}, nil
}
// NewPluginFactory creates a Plugin instance. Called by both Linux entry (main.go)
// and Windows bridge (z_bridge_gen.go) to avoid naming conflict with C export.
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil
}