mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-09-20 00:48:12 +00:00
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:
@ -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
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
I need to rewrite the preamble section. Let me use a python script to make this change.
|
||||
@ -1,13 +0,0 @@
|
||||
# testplugin
|
||||
|
||||
testplugin plugin
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
plugindev build
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
Upload the .hmap file through the Plugin Manager API.
|
||||
@ -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
|
||||
@ -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)
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user