mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-09-20 17:08:01 +00:00
docs: 修正全部文档使其与源码实现一致
- Plugin.Start(sdk *PluginSDK) 接口签名改为指针 - 方法表重写: 移除 CallLLM/QueryKnowledge/SetMemory 等不存在方法 - IOInjector 参数顺序修正为 (source, channel, text) - 删除虚构 SDKConfig, 替换为实际 New() 构造函数签名 - .hmap 内容描述一致化 (plugin.so + plugin.dll + main.lua) - 添加 meta/ 包元数据文件
This commit is contained in:
415
tools/plugindev/cmd_build.go
Normal file
415
tools/plugindev/cmd_build.go
Normal file
@ -0,0 +1,415 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type BuildConfig struct {
|
||||
OutDir string
|
||||
Targets []string // "linux/amd64", "windows/amd64", "lua"
|
||||
}
|
||||
|
||||
func cmdBuild(args []string) {
|
||||
cfg := BuildConfig{OutDir: "dist"}
|
||||
for i := 0; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "--outdir":
|
||||
if i+1 < len(args) {
|
||||
cfg.OutDir = args[i+1]
|
||||
i++
|
||||
}
|
||||
case "--target":
|
||||
if i+1 < len(args) {
|
||||
cfg.Targets = append(cfg.Targets, 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
func readPlgJSON(path string) (*PlgConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var plg PlgConfig
|
||||
if err := json.Unmarshal(data, &plg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &plg, nil
|
||||
}
|
||||
|
||||
func parseTargets(raw string) []string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" || raw == "native" {
|
||||
return nil
|
||||
}
|
||||
var t []string
|
||||
for _, s := range strings.Split(raw, ",") {
|
||||
s = strings.TrimSpace(s)
|
||||
if s != "" {
|
||||
t = append(t, s)
|
||||
}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func writePluginJSON(plg *PlgConfig, entry string) {
|
||||
m := map[string]interface{}{
|
||||
"name": plg.Name,
|
||||
"name_zh": plg.NameZh,
|
||||
"name_en": plg.NameEn,
|
||||
"version": plg.Version,
|
||||
"description": plg.Description,
|
||||
"author": plg.Author,
|
||||
"entry": entry,
|
||||
}
|
||||
if len(plg.Tags) > 0 {
|
||||
m["tags"] = plg.Tags
|
||||
}
|
||||
data, _ := json.MarshalIndent(m, "", " ")
|
||||
os.WriteFile("plugin.json", data, 0644)
|
||||
}
|
||||
|
||||
type buildConfig struct {
|
||||
goos string
|
||||
goarch string
|
||||
entryFile string // "plugin.so" or "plugin.dll"
|
||||
}
|
||||
|
||||
func resolveBuild(target string) (*buildConfig, string) {
|
||||
if target == "lua" || target == "" {
|
||||
return nil, "lua"
|
||||
}
|
||||
|
||||
goos, goarch, _ := strings.Cut(target, "/")
|
||||
if goos == "" {
|
||||
goos = runtime.GOOS
|
||||
if goarch == "" {
|
||||
goarch = runtime.GOARCH
|
||||
}
|
||||
}
|
||||
|
||||
switch goos {
|
||||
case "linux", "darwin", "freebsd", "windows":
|
||||
ext := ".so"
|
||||
if goos == "windows" {
|
||||
ext = ".dll"
|
||||
}
|
||||
return &buildConfig{
|
||||
goos: goos,
|
||||
goarch: goarch,
|
||||
entryFile: "plugin" + ext,
|
||||
}, ""
|
||||
default:
|
||||
return nil, fmt.Sprintf("unsupported OS %q", goos)
|
||||
}
|
||||
}
|
||||
|
||||
func buildTarget(plg *PlgConfig, target, outDir string) {
|
||||
os.MkdirAll(outDir, 0755)
|
||||
|
||||
// Lua: no compilation, package source directly
|
||||
if target == "lua" {
|
||||
writePluginJSON(plg, "main.lua")
|
||||
pkgFiles := []string{"plugin.json", "main.lua"}
|
||||
for _, f := range []string{"README.md", "LICENSE"} {
|
||||
if _, err := os.Stat(f); err == nil {
|
||||
pkgFiles = append(pkgFiles, f)
|
||||
}
|
||||
}
|
||||
// Include thirdpart Lua files
|
||||
if entries, err := os.ReadDir("thirdpart"); err == nil {
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() && strings.HasSuffix(e.Name(), ".lua") {
|
||||
path := filepath.Join("thirdpart", e.Name())
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
pkgFiles = append(pkgFiles, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
hmapName := fmt.Sprintf("%s_lua.hmap", toSnake(plg.NameEn))
|
||||
createHmap(filepath.Join(outDir, hmapName), pkgFiles)
|
||||
fmt.Printf(" packaged %s\n", hmapName)
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve build config
|
||||
cfg, errMsg := resolveBuild(target)
|
||||
if cfg == nil {
|
||||
fmt.Printf(" error: %s\n", errMsg)
|
||||
return
|
||||
}
|
||||
|
||||
buildDir := "build"
|
||||
os.MkdirAll(buildDir, 0755)
|
||||
outPath := filepath.Join(buildDir, cfg.entryFile)
|
||||
|
||||
// Auto-generate C ABI bridge (all platforms use c-shared)
|
||||
bridgeCleanup := generateBridge(cfg.goos)
|
||||
_ = bridgeCleanup // DISABLED cleanup for debug
|
||||
|
||||
// Auto-link thirdpart/ contents
|
||||
thirdpartCleanup := linkThirdpart(target)
|
||||
defer thirdpartCleanup()
|
||||
|
||||
// Write plugin.json with the correct entry for this target
|
||||
writePluginJSON(plg, cfg.entryFile)
|
||||
|
||||
cmd := exec.Command("go", "build", "-buildmode=c-shared", "-o", outPath)
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, "GOOS="+cfg.goos, "GOARCH="+cfg.goarch, "CGO_ENABLED=1")
|
||||
|
||||
// Auto-detect MinGW gcc on Windows
|
||||
if cfg.goos == "windows" {
|
||||
cc := detectWindowsCC()
|
||||
if cc != "" {
|
||||
cmd.Env = append(cmd.Env, "CC="+cc)
|
||||
}
|
||||
}
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
// DEBUG: list files before building
|
||||
entries, _ := os.ReadDir(".")
|
||||
for _, e := range entries {
|
||||
fmt.Printf(" [DEBUG] file: %s\n", e.Name())
|
||||
}
|
||||
|
||||
fmt.Printf(" compiling %s/%s (-buildmode=c-shared)...\n", cfg.goos, cfg.goarch)
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf(" error: build %s/%s: %v\n", cfg.goos, cfg.goarch, err)
|
||||
return
|
||||
}
|
||||
|
||||
// package
|
||||
pkgFiles := []string{"plugin.json", outPath}
|
||||
for _, f := range []string{"README.md", "LICENSE"} {
|
||||
if _, err := os.Stat(f); err == nil {
|
||||
pkgFiles = append(pkgFiles, f)
|
||||
}
|
||||
}
|
||||
hmapName := fmt.Sprintf("%s_%s_%s.hmap", toSnake(plg.NameEn), cfg.goos, cfg.goarch)
|
||||
createHmap(filepath.Join(outDir, hmapName), pkgFiles)
|
||||
fmt.Printf(" packaged %s\n", hmapName)
|
||||
}
|
||||
|
||||
func createHmap(hmapPath string, files []string) {
|
||||
f, err := os.Create(hmapPath)
|
||||
if err != nil {
|
||||
fmt.Printf("error: create hmap %s: %v\n", hmapPath, err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
w := zip.NewWriter(f)
|
||||
defer w.Close()
|
||||
|
||||
for _, path := range files {
|
||||
if path == "" {
|
||||
continue
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
hdr, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
hdr.Method = zip.Deflate
|
||||
hdr.Name = filepath.Base(path)
|
||||
writer, err := w.CreateHeader(hdr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
src, err := os.Open(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
io.Copy(writer, src)
|
||||
src.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func toSnake(s string) string {
|
||||
return strings.ToLower(strings.ReplaceAll(s, " ", "_"))
|
||||
}
|
||||
|
||||
// detectWindowsCC looks for a MinGW-w64 gcc on Windows for c-shared builds.
|
||||
func detectWindowsCC() string {
|
||||
// Check CC from environment first
|
||||
if cc := os.Getenv("CC"); cc != "" {
|
||||
if _, err := exec.LookPath(cc); err == nil {
|
||||
return cc
|
||||
}
|
||||
}
|
||||
// Check common MinGW install paths
|
||||
candidates := []string{
|
||||
"C:\\mingw64\\bin\\gcc.exe",
|
||||
"C:\\MinGW\\bin\\gcc.exe",
|
||||
"C:\\msys64\\mingw64\\bin\\gcc.exe",
|
||||
"C:\\Users\\21989\\AppData\\Local\\Temp\\mingw64\\mingw64\\bin\\gcc.exe",
|
||||
}
|
||||
// Also search PATH for gcc
|
||||
if path, err := exec.LookPath("gcc"); err == nil {
|
||||
return path
|
||||
}
|
||||
for _, c := range candidates {
|
||||
if _, err := os.Stat(c); err == nil {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// stripIncludeGuard strips preprocessor guards and C++ comments from a C header,
|
||||
// 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.
|
||||
// Returns a cleanup function to remove generated files.
|
||||
func generateBridge(goos string) func() {
|
||||
const bridgeFile = "z_bridge_gen.go"
|
||||
const cEntryFile = "z_entry.c"
|
||||
os.Remove(bridgeFile)
|
||||
os.Remove(cEntryFile)
|
||||
|
||||
var files []string
|
||||
|
||||
if goos == "windows" {
|
||||
if err := os.WriteFile(bridgeFile, []byte(tmplBridge), 0644); err != nil {
|
||||
fmt.Printf(" error: write bridge: %v\n", err)
|
||||
return func() {}
|
||||
}
|
||||
files = append(files, bridgeFile)
|
||||
} else {
|
||||
if err := os.WriteFile(bridgeFile, []byte(tmplLinuxBridge), 0644); err != nil {
|
||||
fmt.Printf(" error: write bridge: %v\n", err)
|
||||
return func() {}
|
||||
}
|
||||
files = append(files, bridgeFile)
|
||||
// Write C entry point file
|
||||
if err := os.WriteFile(cEntryFile, []byte(tmplPluginInitC), 0644); err != nil {
|
||||
fmt.Printf(" error: write C entry: %v\n", err)
|
||||
return func() {}
|
||||
}
|
||||
files = append(files, cEntryFile)
|
||||
}
|
||||
|
||||
return func() {
|
||||
for _, f := range files {
|
||||
os.Remove(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// linkThirdpart scans thirdpart/ for source files and generates auto-import stubs.
|
||||
// For Go plugins: if thirdpart/*.go exists, generate z_thirdpart.go with import.
|
||||
// For Lua plugins: no action needed (thirdpart/*.lua is packaged separately in buildTarget).
|
||||
// Returns cleanup function to remove generated files.
|
||||
func linkThirdpart(target string) func() {
|
||||
const thirdpartDir = "thirdpart"
|
||||
const importFile = "z_thirdpart.go"
|
||||
os.Remove(importFile)
|
||||
|
||||
if info, err := os.Stat(thirdpartDir); err != nil || !info.IsDir() {
|
||||
return func() {}
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(thirdpartDir)
|
||||
if err != nil {
|
||||
return func() {}
|
||||
}
|
||||
|
||||
// For Go builds: check for .go files
|
||||
if target != "lua" {
|
||||
hasGo := false
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() && strings.HasSuffix(e.Name(), ".go") {
|
||||
hasGo = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasGo {
|
||||
// Read go.mod to get the module path
|
||||
gomodPath := "go.mod"
|
||||
data, err := os.ReadFile(gomodPath)
|
||||
if err != nil {
|
||||
return func() {}
|
||||
}
|
||||
modulePath := ""
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
if strings.HasPrefix(line, "module ") {
|
||||
modulePath = strings.TrimSpace(line[7:])
|
||||
break
|
||||
}
|
||||
}
|
||||
if modulePath != "" {
|
||||
importPath := modulePath + "/" + thirdpartDir
|
||||
stub := "package main\nimport _ \"" + importPath + "\"\n"
|
||||
os.WriteFile(importFile, []byte(stub), 0644)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return func() {
|
||||
os.Remove(importFile)
|
||||
}
|
||||
}
|
||||
33
tools/plugindev/cmd_clean.go
Normal file
33
tools/plugindev/cmd_clean.go
Normal file
@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func cmdClean(args []string) {
|
||||
outDir := "dist"
|
||||
if len(args) > 0 && args[0] == "--outdir" && len(args) > 1 {
|
||||
outDir = args[1]
|
||||
}
|
||||
|
||||
dirs := []string{"build", outDir}
|
||||
for _, d := range dirs {
|
||||
if _, err := os.Stat(d); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
if err := os.RemoveAll(d); err != nil {
|
||||
fmt.Printf("error: remove %s: %v\n", d, err)
|
||||
} else {
|
||||
fmt.Printf("removed %s/\n", d)
|
||||
}
|
||||
}
|
||||
|
||||
// also clean generated files
|
||||
for _, f := range []string{"plugin.json", "z_bridge_gen.go"} {
|
||||
if _, err := os.Stat(f); err == nil {
|
||||
os.Remove(f)
|
||||
fmt.Printf("removed %s\n", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
134
tools/plugindev/cmd_debug.go
Normal file
134
tools/plugindev/cmd_debug.go
Normal file
@ -0,0 +1,134 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// tmplLuaDebug is the temporary Lua debug script template
|
||||
const tmplLuaDebug = `-- HomeAgent Lua Plugin Debug
|
||||
-- Generated by plugindev debug --lua
|
||||
sdk = require("sdk")
|
||||
local ok, plugin = pcall(dofile, "main.lua")
|
||||
if not ok then
|
||||
print("[debug] ERROR loading main.lua: " .. tostring(plugin))
|
||||
os.exit(1)
|
||||
end
|
||||
if type(plugin) == "table" then
|
||||
print("[debug] Plugin: " .. tostring(plugin.name or "unnamed"))
|
||||
if plugin.start then
|
||||
print("[debug] Calling plugin.start(sdk) ...")
|
||||
local ok, err = pcall(plugin.start, sdk)
|
||||
if ok then
|
||||
print("[debug] plugin.start() OK")
|
||||
else
|
||||
print("[debug] plugin:start() ERROR: " .. tostring(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
print("")
|
||||
print("=== Interactive REPL ===")
|
||||
print("sdk, plugin globals are available")
|
||||
local function repl()
|
||||
while true do
|
||||
io.write("> ")
|
||||
io.flush()
|
||||
local line = io.read()
|
||||
if line == nil or line == "exit" or line == "quit" then break end
|
||||
if line == "help" then
|
||||
print(" help - this help")
|
||||
print(" exit/quit - exit debug")
|
||||
print(" sdk - SDK API table")
|
||||
print(" plugin - loaded plugin table")
|
||||
else
|
||||
local fn, err = (loadstring or load)(line)
|
||||
if fn then
|
||||
local ok, result = pcall(fn)
|
||||
if ok and result ~= nil then print(tostring(result)) end
|
||||
if not ok then print("Error: " .. tostring(result)) end
|
||||
else
|
||||
print("Error: " .. tostring(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
repl()
|
||||
`
|
||||
|
||||
func cmdDebug(args []string) {
|
||||
dir := "."
|
||||
if len(args) > 0 && args[0] != "" {
|
||||
dir = args[0]
|
||||
}
|
||||
|
||||
luaPath := filepath.Join(dir, "main.lua")
|
||||
goPath := filepath.Join(dir, "main.go")
|
||||
sdkPath := filepath.Join(dir, "sdk.lua")
|
||||
|
||||
if _, err := os.Stat(luaPath); err == nil {
|
||||
debugLua(dir, sdkPath, luaPath)
|
||||
} else if _, err := os.Stat(goPath); err == nil {
|
||||
debugGo(dir, goPath)
|
||||
} else {
|
||||
fmt.Println("error: no main.lua or main.go found in", dir)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func debugLua(dir, sdkPath, luaPath string) {
|
||||
// check lua interpreter
|
||||
luaBin, err := exec.LookPath("lua")
|
||||
if err != nil {
|
||||
fmt.Println("error: lua interpreter not found in PATH")
|
||||
fmt.Println(" install Lua 5.1+ or use plugindev build to compile your plugin")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("[debug] Lua interpreter: %s\n", luaBin)
|
||||
fmt.Printf("[debug] Plugin dir: %s\n", dir)
|
||||
|
||||
// check if sdk.lua exists
|
||||
if _, err := os.Stat(sdkPath); os.IsNotExist(err) {
|
||||
fmt.Println("warning: sdk.lua not found, debug SDK mock will not be available")
|
||||
}
|
||||
|
||||
// write temporary debug script
|
||||
debugScript := filepath.Join(dir, "_debug.lua")
|
||||
if err := os.WriteFile(debugScript, []byte(tmplLuaDebug), 0644); err != nil {
|
||||
fmt.Printf("error: write debug script: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer os.Remove(debugScript)
|
||||
|
||||
cmd := exec.Command(luaBin, filepath.Base(debugScript))
|
||||
cmd.Dir = dir
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
fmt.Println("[debug] Starting Lua debug session...")
|
||||
fmt.Println()
|
||||
if err := cmd.Run(); err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
os.Exit(exitErr.ExitCode())
|
||||
}
|
||||
fmt.Printf("[debug] error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func debugGo(dir, goPath string) {
|
||||
fmt.Println("Go debug mode: use standard Go tooling")
|
||||
fmt.Println()
|
||||
fmt.Println(" go test -v ./... # run tests")
|
||||
fmt.Println(" go build -o plugin.so -buildmode=plugin . # build plugin")
|
||||
fmt.Println(" plugindev build # package as .hmap")
|
||||
fmt.Println()
|
||||
fmt.Println("For interactive Go debugging, use your IDE or dlv:")
|
||||
fmt.Println(" dlv debug # Delve debugger")
|
||||
}
|
||||
|
||||
var _ = strings.TrimSpace
|
||||
201
tools/plugindev/cmd_init.go
Normal file
201
tools/plugindev/cmd_init.go
Normal file
@ -0,0 +1,201 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// sdkRoot is the HomeAgent SDK root directory, computed at init time from source location.
|
||||
var sdkRoot string
|
||||
|
||||
const cabiVersion = 1
|
||||
|
||||
func init() {
|
||||
_, filename, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// filename: <sdk_root>/tools/plugindev/cmd_init.go
|
||||
sdkRoot = filepath.Dir(filepath.Dir(filepath.Dir(filename)))
|
||||
}
|
||||
|
||||
type PlgConfig struct {
|
||||
Name string `json:"name"`
|
||||
NameZh string `json:"name_zh"`
|
||||
NameEn string `json:"name_en"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
Author string `json:"author"`
|
||||
Entry string `json:"entry"`
|
||||
Tags []string `json:"tags"`
|
||||
Targets string `json:"targets"`
|
||||
}
|
||||
|
||||
type TemplateData struct {
|
||||
Plg PlgConfig
|
||||
IsLua bool
|
||||
|
||||
// Go module info (for go.mod)
|
||||
ModulePath string
|
||||
GoVersion string
|
||||
SDKModule string
|
||||
SDKVersion string
|
||||
SDKReplace string
|
||||
|
||||
// C ABI
|
||||
CABIVersion int
|
||||
CABIHeader string
|
||||
}
|
||||
|
||||
func cmdInit(args []string) {
|
||||
if len(args) < 1 {
|
||||
fmt.Println("Usage: plugindev init <name> [--lua]")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
name := args[0]
|
||||
isLua := false
|
||||
for _, a := range args[1:] {
|
||||
switch a {
|
||||
case "--lua":
|
||||
isLua = true
|
||||
}
|
||||
}
|
||||
|
||||
dir := name
|
||||
if _, err := os.Stat(dir); !os.IsNotExist(err) {
|
||||
fmt.Printf("error: directory %q already exists\n", dir)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
entry := "plugin.so"
|
||||
var targets string
|
||||
if isLua {
|
||||
entry = "main.lua"
|
||||
targets = "lua"
|
||||
} else {
|
||||
targets = "linux/amd64,windows/amd64"
|
||||
}
|
||||
|
||||
nameEn := strings.ReplaceAll(name, "-", " ")
|
||||
nameEn = strings.Title(nameEn)
|
||||
|
||||
data := TemplateData{
|
||||
Plg: PlgConfig{
|
||||
Name: name,
|
||||
NameZh: "中文名",
|
||||
NameEn: nameEn,
|
||||
Version: "0.1.0",
|
||||
Description: name + " plugin",
|
||||
Author: "HomeAgent",
|
||||
Entry: entry,
|
||||
Tags: []string{name},
|
||||
Targets: targets,
|
||||
},
|
||||
IsLua: isLua,
|
||||
CABIVersion: cabiVersion,
|
||||
CABIHeader: tmplCABIHeader,
|
||||
}
|
||||
|
||||
// Detect SDK info for Go plugin go.mod
|
||||
if !isLua {
|
||||
sdkMod, goVer, sdkPath := detectSDKInfo()
|
||||
sdkReplace := sdkPath
|
||||
// Make replace path absolute and use forward slashes
|
||||
if abs, err := filepath.Abs(sdkPath); err == nil {
|
||||
sdkReplace = strings.ReplaceAll(abs, "\\", "/")
|
||||
}
|
||||
data.ModulePath = name
|
||||
data.GoVersion = goVer
|
||||
data.SDKModule = sdkMod
|
||||
data.SDKVersion = "v0.0.0"
|
||||
data.SDKReplace = sdkReplace
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
fmt.Printf("error: create dir: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// write plg.json
|
||||
writeTemplate(filepath.Join(dir, "plg.json"), tmplPlgJSON, data)
|
||||
|
||||
// Lua plugins get main.lua + sdk.lua; Go plugins get plugin.go only
|
||||
if isLua {
|
||||
writeTemplate(filepath.Join(dir, "main.lua"), tmplMainLua, data)
|
||||
writeTemplate(filepath.Join(dir, "sdk.lua"), tmplSDKLua, data)
|
||||
} else {
|
||||
writeTemplate(filepath.Join(dir, "plugin.go"), tmplPluginGo, data)
|
||||
}
|
||||
|
||||
// write README.md
|
||||
writeTemplate(filepath.Join(dir, "README.md"), tmplReadme, data)
|
||||
|
||||
// write go.mod for Go plugins
|
||||
if !isLua {
|
||||
writeTemplate(filepath.Join(dir, "go.mod"), tmplGoMod, data)
|
||||
}
|
||||
|
||||
// create thirdpart directory for external library sources
|
||||
os.MkdirAll(filepath.Join(dir, "thirdpart"), 0755)
|
||||
|
||||
fmt.Printf("Created plugin project %q (%s)\n", dir, entry)
|
||||
if isLua {
|
||||
fmt.Printf(" cd %s && lua main.lua (standalone test)\n", dir)
|
||||
}
|
||||
fmt.Printf(" cd %s && plugindev build\n", dir)
|
||||
}
|
||||
|
||||
// detectSDKInfo reads the HomeAgent SDK's go.mod to get module path and go version.
|
||||
func detectSDKInfo() (modulePath, goVersion, sdkPath string) {
|
||||
if sdkRoot == "" {
|
||||
fmt.Printf("error: cannot detect SDK root (built outside SDK tree?)\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
gomodPath := filepath.Join(sdkRoot, "go.mod")
|
||||
data, err := os.ReadFile(gomodPath)
|
||||
if err != nil {
|
||||
fmt.Printf("error: cannot read SDK go.mod at %s: %v\n", gomodPath, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "module ") {
|
||||
modulePath = strings.TrimSpace(line[7:])
|
||||
}
|
||||
if strings.HasPrefix(line, "go ") {
|
||||
goVersion = strings.TrimSpace(line[3:])
|
||||
}
|
||||
}
|
||||
if modulePath == "" {
|
||||
fmt.Printf("error: no module directive in %s\n", gomodPath)
|
||||
os.Exit(1)
|
||||
}
|
||||
if goVersion == "" {
|
||||
goVersion = "1.21"
|
||||
}
|
||||
return modulePath, goVersion, sdkRoot
|
||||
}
|
||||
|
||||
func writeTemplate(path, content string, data TemplateData) {
|
||||
tmpl, err := template.New("").Parse(content)
|
||||
if err != nil {
|
||||
fmt.Printf("error: parse template: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
fmt.Printf("error: create %s: %v\n", path, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer f.Close()
|
||||
if err := tmpl.Execute(f, data); err != nil {
|
||||
fmt.Printf("error: execute template: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
3
tools/plugindev/go.mod
Normal file
3
tools/plugindev/go.mod
Normal file
@ -0,0 +1,3 @@
|
||||
module github.com/JianFeeeee/homeagent-sdk/tools/plugindev
|
||||
|
||||
go 1.21
|
||||
41
tools/plugindev/main.go
Normal file
41
tools/plugindev/main.go
Normal file
@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
help()
|
||||
return
|
||||
}
|
||||
switch os.Args[1] {
|
||||
case "init":
|
||||
cmdInit(os.Args[2:])
|
||||
case "build":
|
||||
cmdBuild(os.Args[2:])
|
||||
case "clean":
|
||||
cmdClean(os.Args[2:])
|
||||
case "debug":
|
||||
cmdDebug(os.Args[2:])
|
||||
default:
|
||||
help()
|
||||
}
|
||||
}
|
||||
|
||||
func help() {
|
||||
fmt.Println(`HomeAgent Plugin Dev Tool
|
||||
|
||||
Usage:
|
||||
plugindev init <name> Scaffold a new plugin project
|
||||
plugindev build [flags] Compile and package plugin
|
||||
plugindev clean Clean build/dist artifacts
|
||||
plugindev debug [dir] Interpret and debug plugin source
|
||||
|
||||
Flags:
|
||||
--outdir Output directory (default: dist)
|
||||
--target Target OS/arch (e.g. linux/amd64), repeatable
|
||||
--lua Create Lua plugin (for init)
|
||||
`)
|
||||
}
|
||||
3
tools/plugindev/template_c.go
Normal file
3
tools/plugindev/template_c.go
Normal file
@ -0,0 +1,3 @@
|
||||
package main
|
||||
|
||||
// tmplPluginInitC is in templates.go (moved to keep all C ABI together)
|
||||
727
tools/plugindev/templates.go
Normal file
727
tools/plugindev/templates.go
Normal file
@ -0,0 +1,727 @@
|
||||
package main
|
||||
|
||||
// tmplPlgJSON is the plg.json template
|
||||
const tmplPlgJSON = `{
|
||||
"name": "{{.Plg.Name}}",
|
||||
"name_zh": "{{.Plg.NameZh}}",
|
||||
"name_en": "{{.Plg.NameEn}}",
|
||||
"version": "{{.Plg.Version}}",
|
||||
"description": "{{.Plg.Description}}",
|
||||
"author": "{{.Plg.Author}}",
|
||||
"entry": "{{.Plg.Entry}}",
|
||||
"tags": [{{range $i, $t := .Plg.Tags}}{{if $i}}, {{end}}"{{$t}}"{{end}}],
|
||||
"targets": "{{.Plg.Targets}}"
|
||||
}
|
||||
`
|
||||
|
||||
const tmplGoMod = `module {{.ModulePath}}
|
||||
|
||||
go {{.GoVersion}}
|
||||
|
||||
require {{.SDKModule}} {{.SDKVersion}}
|
||||
|
||||
replace {{.SDKModule}} => {{.SDKReplace}}
|
||||
`
|
||||
|
||||
const tmplPluginGo = `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.{{.Plg.Name}}.example", Default: "hello", Type: "string",
|
||||
DisplayName: "示例配置", Description: "An example configuration key",
|
||||
Category: "{{.Plg.Name}}",
|
||||
})
|
||||
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 {{.Plg.Name}} plugin!"}, nil
|
||||
}
|
||||
|
||||
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return &Plugin{name: name}, nil
|
||||
}
|
||||
`
|
||||
|
||||
const tmplSDKLua = `-- HomeAgent Lua Plugin SDK (standalone mock)
|
||||
sdk = {}
|
||||
function sdk.log(level, msg) print("[lua-plugin] " .. tostring(level) .. ": " .. tostring(msg)) end
|
||||
function sdk.register_tool(name, def, handler) print("[lua-plugin] register_tool: " .. tostring(name)) end
|
||||
function sdk.register_stage(stage, handler) print("[lua-plugin] register_stage: " .. tostring(stage)) end
|
||||
function sdk.register_api(name) print("[lua-plugin] register_api: " .. tostring(name)) end
|
||||
function sdk.get_setting(key) return nil end
|
||||
function sdk.set_setting(key, value) print("[lua-plugin] set_setting: " .. tostring(key)) end
|
||||
function sdk.inject_text(source, channel, text) print("[lua-plugin] inject_text: " .. tostring(source)) end
|
||||
function sdk.inject_interrupt(source, channel, text) print("[lua-plugin] inject_interrupt: " .. tostring(source)) end
|
||||
function sdk.inject_text_no_memory(source, channel, text) print("[lua-plugin] inject_text_no_memory: " .. tostring(source)) end
|
||||
sdk.json = {}
|
||||
function sdk.json.encode(val)
|
||||
if type(val) == "string" then return '"' .. val:gsub('"', '\\"'):gsub('\n', '\\n') .. '"'
|
||||
elseif type(val) == "number" or type(val) == "boolean" then return tostring(val)
|
||||
elseif type(val) == "table" then local parts, i = {}, 1
|
||||
for k, v in pairs(val) do parts[i] = sdk.json.encode(k) .. ":" .. sdk.json.encode(v); i = i + 1 end
|
||||
return "{" .. table.concat(parts, ",") .. "}" end
|
||||
return "null"
|
||||
end
|
||||
function sdk.json.decode(str) local ok, fn = pcall(load, "return " .. str); if ok then return fn() end; return nil end
|
||||
sdk.http = {}
|
||||
function sdk.http.get(url) print("[lua-plugin] http.get: " .. tostring(url)); return {status=200, body='{"mock":true}', headers={}} end
|
||||
function sdk.http.post(url, body, ct) print("[lua-plugin] http.post: " .. tostring(url)); return {status=200, body='{"mock":true}', headers={}} end
|
||||
return sdk
|
||||
`
|
||||
|
||||
const tmplMainLua = `-- {{.Plg.Name}} plugin
|
||||
local plugin = { name = "{{.Plg.Name}}" }
|
||||
function plugin.start(sdk)
|
||||
sdk.log("info", "{{.Plg.Name}} starting...")
|
||||
sdk.register_tool("{{.Plg.Name}}_hello", {
|
||||
description = "A hello world tool",
|
||||
parameters = { type = "object", properties = {} }
|
||||
}, function(args) return { content = "Hello from {{.Plg.Name}} plugin!" } end)
|
||||
sdk.log("info", "{{.Plg.Name}} started")
|
||||
end
|
||||
function plugin.stop() sdk.log("info", "{{.Plg.Name}} stopped") end
|
||||
return plugin
|
||||
`
|
||||
|
||||
// tmplBridge — Windows DLL C ABI bridge (unchanged)
|
||||
const tmplBridge = `//go:build windows && cgo
|
||||
|
||||
package main
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"unsafe"
|
||||
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
handleMap = map[unsafe.Pointer]*bridgeState{}
|
||||
)
|
||||
|
||||
type bridgeState struct {
|
||||
plugin sdk.Plugin
|
||||
toolDefs map[string]sdk.ToolDef
|
||||
handlers map[string]sdk.ToolHandler
|
||||
stages map[string]sdk.StageHandler
|
||||
settings map[string]interface{}
|
||||
}
|
||||
|
||||
func newHandle(plg sdk.Plugin) unsafe.Pointer {
|
||||
mu.Lock(); defer mu.Unlock()
|
||||
h := C.malloc(C.size_t(1))
|
||||
handleMap[h] = &bridgeState{
|
||||
plugin: plg, toolDefs: make(map[string]sdk.ToolDef),
|
||||
handlers: make(map[string]sdk.ToolHandler), stages: make(map[string]sdk.StageHandler),
|
||||
settings: make(map[string]interface{}),
|
||||
}
|
||||
return h
|
||||
}
|
||||
func getState(h unsafe.Pointer) *bridgeState { mu.Lock(); defer mu.Unlock(); return handleMap[h] }
|
||||
func delState(h unsafe.Pointer) { mu.Lock(); defer mu.Unlock(); delete(handleMap, h); C.free(h) }
|
||||
|
||||
//export NewPlugin
|
||||
func NewPlugin(name *C.char, configJSON *C.char) unsafe.Pointer {
|
||||
goName := C.GoString(name)
|
||||
var config map[string]interface{}
|
||||
if configJSON != nil {
|
||||
var wrapper map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(C.GoString(configJSON)), &wrapper); err == nil {
|
||||
if c, ok := wrapper["config"].(map[string]interface{}); ok { config = c }
|
||||
}
|
||||
}
|
||||
plg, err := NewPlugin(goName, config)
|
||||
if err != nil { return nil }
|
||||
return newHandle(plg)
|
||||
}
|
||||
|
||||
//export StartPlugin
|
||||
func StartPlugin(handle unsafe.Pointer) C.int {
|
||||
bs := getState(handle)
|
||||
if bs == nil { return 1 }
|
||||
mockSett := &bridgeSettings{data: bs.settings}
|
||||
mockSDK := sdk.New(bs.plugin.Name(), mockSett,
|
||||
func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error {
|
||||
bs.toolDefs[name] = def; bs.handlers[name] = handler; return nil
|
||||
},
|
||||
func(stage sdk.Stage, handler sdk.StageHandler) { bs.stages[string(stage)] = handler },
|
||||
func(name string) error { return nil },
|
||||
)
|
||||
if err := bs.plugin.Start(mockSDK); err != nil { return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
//export StopPlugin
|
||||
func StopPlugin(handle unsafe.Pointer) C.int {
|
||||
bs := getState(handle)
|
||||
if bs == nil { return 1 }
|
||||
if err := bs.plugin.Stop(); err != nil { return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
//export DestroyPlugin
|
||||
func DestroyPlugin(handle unsafe.Pointer) {
|
||||
if bs := getState(handle); bs != nil { delState(handle) }
|
||||
}
|
||||
|
||||
//export GetToolDefsJSON
|
||||
func GetToolDefsJSON(handle unsafe.Pointer) *C.char {
|
||||
bs := getState(handle)
|
||||
if bs == nil { return nil }
|
||||
defs := make([]sdk.ToolDef, 0, len(bs.toolDefs))
|
||||
for _, def := range bs.toolDefs { defs = append(defs, def) }
|
||||
b, _ := json.Marshal(defs)
|
||||
return C.CString(string(b))
|
||||
}
|
||||
|
||||
//export InvokeToolJSON
|
||||
func InvokeToolJSON(handle unsafe.Pointer, toolName *C.char, argsJSON *C.char) *C.char {
|
||||
bs := getState(handle)
|
||||
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)) }
|
||||
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)) }
|
||||
b, _ := json.Marshal(r)
|
||||
return C.CString(string(b))
|
||||
}
|
||||
|
||||
//export GetStagesJSON
|
||||
func GetStagesJSON(handle unsafe.Pointer) *C.char {
|
||||
bs := getState(handle)
|
||||
if bs == nil { return nil }
|
||||
type se struct { Stage string ` + "`" + `json:"stage"` + "`" + ` }
|
||||
var entries []se
|
||||
for s := range bs.stages { entries = append(entries, se{s}) }
|
||||
b, _ := json.Marshal(entries)
|
||||
return C.CString(string(b))
|
||||
}
|
||||
|
||||
//export InvokeStage
|
||||
func InvokeStage(handle unsafe.Pointer, stage *C.char, contextJSON *C.char) C.int {
|
||||
bs := getState(handle)
|
||||
if bs == nil || stage == nil { return 1 }
|
||||
goStage := C.GoString(stage)
|
||||
handler, ok := bs.stages[goStage]
|
||||
if !ok { return 1 }
|
||||
var ctx map[string]interface{}
|
||||
if contextJSON != nil { json.Unmarshal([]byte(C.GoString(contextJSON)), &ctx) }
|
||||
sc := &sdk.StageContext{}
|
||||
if ctx != nil {
|
||||
if v, ok := ctx["raw_message"].(string); ok { sc.RawMessage = v }
|
||||
if v, ok := ctx["user_id"].(string); ok { sc.UserID = v }
|
||||
if v, ok := ctx["phase"].(string); ok { sc.Phase = sdk.Stage(v) }
|
||||
}
|
||||
if err := handler(sc); err != nil { return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
//export FreeCString
|
||||
func FreeCString(s *C.char) { C.free(unsafe.Pointer(s)) }
|
||||
|
||||
type bridgeSettings struct{ data map[string]interface{} }
|
||||
func (s *bridgeSettings) Get(key string) (interface{}, error) { v, ok := s.data[key]; if !ok { return nil, nil }; return v, nil }
|
||||
func (s *bridgeSettings) Set(key string, value interface{}) error { s.data[key] = value; return nil }
|
||||
func (s *bridgeSettings) List(prefix string) ([]string, error) {
|
||||
var keys []string
|
||||
for k := range s.data { if len(k) >= len(prefix) && k[:len(prefix)] == prefix { keys = append(keys, k) } }
|
||||
return keys, nil
|
||||
}
|
||||
func (s *bridgeSettings) GetCore(key string) (interface{}, error) { return nil, nil }
|
||||
func (s *bridgeSettings) SetCore(key string, value interface{}) error { return nil }
|
||||
func (s *bridgeSettings) ListCore(prefix string) ([]string, error) { return nil, nil }
|
||||
func (s *bridgeSettings) GetPlugin(plugin, key string) (interface{}, error) { return nil, nil }
|
||||
func (s *bridgeSettings) SetPlugin(plugin, key string, value interface{}) error { return nil }
|
||||
func (s *bridgeSettings) ListPlugin(plugin, prefix string) ([]string, error) { return nil, nil }
|
||||
func (s *bridgeSettings) RegisterDef(def sdk.ConfigDef) {}
|
||||
func (s *bridgeSettings) Defs(prefix string) []*sdk.ConfigDef { return nil }
|
||||
func (s *bridgeSettings) Dump() map[string]interface{} { return s.data }
|
||||
func (s *bridgeSettings) Plugins() []string { return nil }
|
||||
|
||||
func main() {}
|
||||
`
|
||||
|
||||
// tmplCABIHeader — shared C ABI type definitions for both core and plugin
|
||||
const tmplCABIHeader = `
|
||||
#ifndef HOMEAGENT_CABI_H
|
||||
#define HOMEAGENT_CABI_H
|
||||
#define HOMEAGENT_ABI_VERSION 1
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// PluginAPI — implemented by the plugin, called by the core
|
||||
typedef struct {
|
||||
int version; int version_min;
|
||||
int (*init_plugin)(char*, char*, char**);
|
||||
int (*start_plugin)(void*, int, char**);
|
||||
int (*stop_plugin)(char**);
|
||||
int (*invoke_tool)(char*, char*, char**, char**);
|
||||
int (*invoke_stage)(char*, char*, char**);
|
||||
int (*invoke_output)(char*, char*, char*, char**);
|
||||
void (*free_string)(char*);
|
||||
} PluginAPI;
|
||||
|
||||
// CoreAPI — implemented by the core, passed to plugin via start_plugin
|
||||
// Uses single dispatch function to avoid function pointer ABI issues
|
||||
typedef struct {
|
||||
int version; int version_min;
|
||||
int (*dispatch)(int method_id, void* ctx, char* s1, char* s2, char* s3, int i1, int i2, char** result, char** error);
|
||||
void* ctx;
|
||||
} CoreAPI;
|
||||
|
||||
// Dispatch method IDs (plugin→core SDK calls)
|
||||
enum {
|
||||
CORE_REGISTER_TOOL = 1,
|
||||
CORE_REGISTER_STAGE = 2,
|
||||
CORE_REGISTER_OUTPUT_CH = 3,
|
||||
CORE_REGISTER_PLUGIN_API = 4,
|
||||
CORE_INJECT_TEXT = 5,
|
||||
CORE_INJECT_INTERRUPT_TEXT = 6,
|
||||
CORE_INJECT_TEXT_NO_MEMORY = 7,
|
||||
CORE_SET_AUTO_RESTART = 8,
|
||||
CORE_MEMORY_RECALL = 9,
|
||||
CORE_MEMORY_COMMIT = 10,
|
||||
CORE_MEMORY_INTROSPECT = 11,
|
||||
CORE_MEMORY_MERGE = 12,
|
||||
CORE_MEMORY_PURGE = 13,
|
||||
CORE_DOC_QUERY = 14,
|
||||
CORE_KNOWLEDGE_SEARCH = 15,
|
||||
CORE_SETTINGS_GET = 16,
|
||||
CORE_SETTINGS_SET = 17,
|
||||
CORE_SETTINGS_REGISTER_DEF = 18,
|
||||
CORE_LLM_LIST_SOURCES = 19,
|
||||
CORE_LLM_SET_SOURCE = 20,
|
||||
CORE_SOCIAL_GET_PERSON = 21,
|
||||
CORE_SOCIAL_GET_NETWORK = 22,
|
||||
CORE_SUBSCRIBE = 23,
|
||||
CORE_UNSUBSCRIBE = 24,
|
||||
CORE_FREE_STRING = 25,
|
||||
CORE_SETTINGS_GET_CORE = 26,
|
||||
CORE_SETTINGS_SET_CORE = 27,
|
||||
CORE_SETTINGS_LIST_CORE = 28,
|
||||
CORE_SETTINGS_GET_PLUGIN = 29,
|
||||
CORE_SETTINGS_SET_PLUGIN = 30,
|
||||
CORE_SETTINGS_LIST_PLUGIN = 31,
|
||||
CORE_DOC_INSERT = 32,
|
||||
CORE_DOC_REMOVE = 33,
|
||||
CORE_DOC_STATS = 34,
|
||||
CORE_KNOWLEDGE_ADD = 35,
|
||||
CORE_KNOWLEDGE_LIST = 36,
|
||||
CORE_LLM_CURRENT_SOURCE = 37,
|
||||
CORE_SOCIAL_GET_TRAIT = 38,
|
||||
CORE_SOCIAL_GET_RELATIONS = 39,
|
||||
CORE_SOCIAL_LIST_PERSONS = 40,
|
||||
CORE_TEXT_MEMORY_APPEND = 41,
|
||||
CORE_SETTINGS_LIST = 42,
|
||||
CORE_SETTINGS_DEFS = 43,
|
||||
CORE_SETTINGS_DUMP = 44,
|
||||
CORE_SETTINGS_PLUGINS = 45,
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
`
|
||||
|
||||
// tmplLinuxBridge — auto-generated Go bridge for Linux c-shared builds.
|
||||
// Called by plugin's Start() with a PluginSDK that wraps CoreAPI dispatch.
|
||||
// PluginSDK calls go through C ABI → CoreAPI dispatch → core's Go PluginSDK.
|
||||
const tmplLinuxBridge = `package main
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
int ha_dispatch(int method_id, void* core_api, char* s1, char* s2, char* s3, int i1, int i2, char** result, char** error);
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"unsafe"
|
||||
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
)
|
||||
|
||||
// ---- global state ----
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
currentPlg sdk.Plugin
|
||||
coreAPI unsafe.Pointer
|
||||
|
||||
handlerMu sync.RWMutex
|
||||
toolHandlers = map[string]sdk.ToolHandler{}
|
||||
stageHandlers = map[string]sdk.StageHandler{}
|
||||
outputHandlers = map[string]sdk.ToolHandler{}
|
||||
)
|
||||
|
||||
// ---- CoreAPI dispatch helpers ----
|
||||
|
||||
func callVoid(methodID int, s1, s2, s3 string, i1, i2 int) error {
|
||||
var c1, c2, c3 *C.char
|
||||
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 s3 != "" { c3 = C.CString(s3); defer C.free(unsafe.Pointer(c3)) }
|
||||
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 {
|
||||
return fmt.Errorf("%s", C.GoString(cErr))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func callString(methodID int, s1, s2, s3 string, i1, i2 int) (string, error) {
|
||||
var c1, c2, c3 *C.char
|
||||
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 s3 != "" { c3 = C.CString(s3); defer C.free(unsafe.Pointer(c3)) }
|
||||
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 {
|
||||
return "", fmt.Errorf("%s", C.GoString(cErr))
|
||||
}
|
||||
if strResult != nil {
|
||||
result := C.GoString(strResult)
|
||||
C.ha_dispatch(C.int(25), coreAPI, strResult, nil, nil, 0, 0, nil, nil)
|
||||
return result, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// ---- buildPluginSDK: PluginSDK backed by CoreAPI dispatch ----
|
||||
// - ALL SDK methods route through C ABI → CoreAPI → core's PluginSDK
|
||||
// - Handlers for tools/stages/output are stored locally AND registered via dispatch
|
||||
|
||||
func buildPluginSDK(name string) *sdk.PluginSDK {
|
||||
sett := &dispatchSettings{}
|
||||
base := sdk.New(name, sett,
|
||||
func(toolName string, def sdk.ToolDef, handler sdk.ToolHandler) error {
|
||||
handlerMu.Lock()
|
||||
toolHandlers[toolName] = handler
|
||||
handlerMu.Unlock()
|
||||
b, _ := json.Marshal(def)
|
||||
return callVoid(1, toolName, string(b), "", 0, 0)
|
||||
},
|
||||
func(stage sdk.Stage, handler sdk.StageHandler) {
|
||||
handlerMu.Lock()
|
||||
stageHandlers[string(stage)] = handler
|
||||
handlerMu.Unlock()
|
||||
callVoid(2, string(stage), "", "", 0, 0)
|
||||
},
|
||||
func(name string) error { return callVoid(4, name, "", "", 0, 0) },
|
||||
func(name string, caps int, desc string, handler sdk.ToolHandler) error {
|
||||
handlerMu.Lock()
|
||||
outputHandlers[name] = handler
|
||||
handlerMu.Unlock()
|
||||
return callVoid(3, name, desc, "", caps, 0)
|
||||
},
|
||||
)
|
||||
base.SetIOInjector(dispatchIO{})
|
||||
base.SetMemoryAPI(dispatchMemory{})
|
||||
base.SetDocMemoryAPI(dispatchDocMemory{})
|
||||
base.SetKnowledgeAPI(dispatchKnowledge{})
|
||||
base.SetLLMAPI(dispatchLLM{})
|
||||
base.SetSocialAPI(dispatchSocial{})
|
||||
base.SetTextMemoryAPI(dispatchTextMemory{})
|
||||
return base
|
||||
}
|
||||
|
||||
// ---- dispatch IO (inline definitions) ----
|
||||
|
||||
type dispatchIO struct{}
|
||||
func (dispatchIO) InjectInterruptText(s, c, t string) { callVoid(6, s, c, t, 0, 0) }
|
||||
func (dispatchIO) InjectText(s, c, t string) { callVoid(5, s, c, t, 0, 0) }
|
||||
func (dispatchIO) InjectTextNoMemory(s, c, t string) { callVoid(7, s, c, t, 0, 0) }
|
||||
|
||||
type dispatchMemory struct{}
|
||||
func (dispatchMemory) Recall(q []string, d int) ([]sdk.Entity, []sdk.Relation, error) {
|
||||
b, _ := json.Marshal(q); r, e := callString(9, string(b), "", "", d, 0)
|
||||
if e != nil || r == "" { return nil, nil, e }
|
||||
var v struct{ Entities []sdk.Entity; Relations []sdk.Relation }
|
||||
if e = json.Unmarshal([]byte(r), &v); e != nil { return nil, nil, e }
|
||||
if v.Entities == nil { v.Entities = []sdk.Entity{} }
|
||||
if v.Relations == nil { v.Relations = []sdk.Relation{} }
|
||||
return v.Entities, v.Relations, nil
|
||||
}
|
||||
func (dispatchMemory) Commit(t []sdk.Triple) error { b, _ := json.Marshal(t); return callVoid(10, string(b), "", "", 0, 0) }
|
||||
func (dispatchMemory) Introspect() (map[string]interface{}, error) { r, e := callString(11, "", "", "", 0, 0); if e != nil || r == "" { return nil, e }; var m map[string]interface{}; return m, json.Unmarshal([]byte(r), &m) }
|
||||
func (dispatchMemory) MergeEntities(s, t string) (int, error) { return 1, callVoid(12, s, t, "", 0, 0) }
|
||||
func (dispatchMemory) Purge(c map[string]string, m string) (int, error) { b, _ := json.Marshal(c); i := 0; if m == "hard" { i = 1 }; return 1, callVoid(13, string(b), "", "", i, 0) }
|
||||
|
||||
type dispatchDocMemory struct{}
|
||||
func (dispatchDocMemory) Query(t string, k int) []*sdk.Doc { r, e := callString(14, t, "", "", k, 0); if e != nil || r == "" { return nil }; var d []*sdk.Doc; json.Unmarshal([]byte(r), &d); return d }
|
||||
func (dispatchDocMemory) Insert(doc *sdk.Doc) error { b, _ := json.Marshal(doc); return callVoid(32, string(b), "", "", 0, 0) }
|
||||
func (dispatchDocMemory) Remove(id string) { callVoid(33, id, "", "", 0, 0) }
|
||||
func (dispatchDocMemory) Stats() map[string]interface{} { r, e := callString(34, "", "", "", 0, 0); if e != nil || r == "" { return nil }; var m map[string]interface{}; json.Unmarshal([]byte(r), &m); return m }
|
||||
|
||||
type dispatchKnowledge struct{}
|
||||
func (dispatchKnowledge) Search(q string, k int) ([]*sdk.Knowledge, error) { r, e := callString(15, q, "", "", k, 0); if e != nil || r == "" { return nil, e }; var v []*sdk.Knowledge; return v, json.Unmarshal([]byte(r), &v) }
|
||||
func (dispatchKnowledge) Add(n, c string) error { return callVoid(35, n, c, "", 0, 0) }
|
||||
func (dispatchKnowledge) List() ([]string, error) { r, e := callString(36, "", "", "", 0, 0); if e != nil || r == "" { return nil, e }; var v []string; return v, json.Unmarshal([]byte(r), &v) }
|
||||
|
||||
type dispatchLLM struct{}
|
||||
func (dispatchLLM) ListSources() []string { r, e := callString(19, "", "", "", 0, 0); if e != nil || r == "" { return nil }; var v []string; json.Unmarshal([]byte(r), &v); return v }
|
||||
func (dispatchLLM) SetSource(n string) error { return callVoid(20, n, "", "", 0, 0) }
|
||||
func (dispatchLLM) CurrentSource() string { r, e := callString(37, "", "", "", 0, 0); if e != nil || r == "" { return "" }; return r }
|
||||
|
||||
type dispatchSocial struct{}
|
||||
func (dispatchSocial) GetPerson(n string) (*sdk.PersonProfile, error) { r, e := callString(21, n, "", "", 0, 0); if e != nil || r == "" { return nil, e }; var v sdk.PersonProfile; return &v, json.Unmarshal([]byte(r), &v) }
|
||||
func (dispatchSocial) GetTrait(n, t string) (string, bool) { r, e := callString(38, n, t, "", 0, 0); if e != nil || r == "" { return "", false }; var m map[string]interface{}; json.Unmarshal([]byte(r), &m); v, _ := m["value"].(string); ok, _ := m["found"].(bool); return v, ok }
|
||||
func (dispatchSocial) GetRelations(name string) ([]sdk.SocialRelation, error) { r, e := callString(39, name, "", "", 0, 0); if e != nil || r == "" { return nil, e }; var v []sdk.SocialRelation; return v, json.Unmarshal([]byte(r), &v) }
|
||||
func (dispatchSocial) GetNetwork(n string, d int) ([]*sdk.PersonProfile, error) { r, e := callString(22, n, "", "", d, 0); if e != nil || r == "" { return nil, e }; var v []*sdk.PersonProfile; return v, json.Unmarshal([]byte(r), &v) }
|
||||
func (dispatchSocial) ListPersons() ([]string, error) { r, e := callString(40, "", "", "", 0, 0); if e != nil || r == "" { return nil, e }; var v []string; return v, json.Unmarshal([]byte(r), &v) }
|
||||
|
||||
type dispatchTextMemory struct{}
|
||||
func (dispatchTextMemory) Append(evt sdk.TextEvent) error { b, _ := json.Marshal(evt); return callVoid(41, string(b), "", "", 0, 0) }
|
||||
|
||||
// ---- dispatchSettings (inline) ----
|
||||
|
||||
type dispatchSettings struct{}
|
||||
func (d *dispatchSettings) Get(key string) (interface{}, error) {
|
||||
r, e := callString(16, key, "", "", 0, 0); if e != nil || r == "" { return nil, e }; var v interface{}; return v, json.Unmarshal([]byte(r), &v)
|
||||
}
|
||||
func (d *dispatchSettings) Set(key string, value interface{}) error {
|
||||
b, _ := json.Marshal(value); return callVoid(17, key, string(b), "", 0, 0)
|
||||
}
|
||||
func (d *dispatchSettings) RegisterDef(def sdk.ConfigDef) { b, _ := json.Marshal(def); callVoid(18, string(b), "", "", 0, 0) }
|
||||
func (d *dispatchSettings) List(prefix string) ([]string, error) {
|
||||
r, e := callString(42, prefix, "", "", 0, 0); if e != nil || r == "" { return nil, e }; var v []string; return v, json.Unmarshal([]byte(r), &v)
|
||||
}
|
||||
func (d *dispatchSettings) GetCore(key string) (interface{}, error) {
|
||||
r, e := callString(26, key, "", "", 0, 0); if e != nil || r == "" { return nil, e }; var v interface{}; return v, json.Unmarshal([]byte(r), &v)
|
||||
}
|
||||
func (d *dispatchSettings) SetCore(key string, value interface{}) error {
|
||||
b, _ := json.Marshal(value); return callVoid(27, key, string(b), "", 0, 0)
|
||||
}
|
||||
func (d *dispatchSettings) ListCore(prefix string) ([]string, error) {
|
||||
r, e := callString(28, prefix, "", "", 0, 0); if e != nil || r == "" { return nil, e }; var v []string; return v, json.Unmarshal([]byte(r), &v)
|
||||
}
|
||||
func (d *dispatchSettings) GetPlugin(plugin, key string) (interface{}, error) {
|
||||
r, e := callString(29, plugin, key, "", 0, 0); if e != nil || r == "" { return nil, e }; var v interface{}; return v, json.Unmarshal([]byte(r), &v)
|
||||
}
|
||||
func (d *dispatchSettings) SetPlugin(plugin, key string, value interface{}) error {
|
||||
b, _ := json.Marshal(value); return callVoid(30, plugin, key, string(b), 0, 0)
|
||||
}
|
||||
func (d *dispatchSettings) ListPlugin(plugin, prefix string) ([]string, error) {
|
||||
r, e := callString(31, plugin, prefix, "", 0, 0); if e != nil || r == "" { return nil, e }; var v []string; return v, json.Unmarshal([]byte(r), &v)
|
||||
}
|
||||
func (d *dispatchSettings) Defs(prefix string) []*sdk.ConfigDef {
|
||||
r, e := callString(43, prefix, "", "", 0, 0); if e != nil || r == "" { return nil }; var v []*sdk.ConfigDef; json.Unmarshal([]byte(r), &v); return v
|
||||
}
|
||||
func (d *dispatchSettings) Dump() map[string]interface{} {
|
||||
r, e := callString(44, "", "", "", 0, 0); if e != nil || r == "" { return nil }; var m map[string]interface{}; json.Unmarshal([]byte(r), &m); return m
|
||||
}
|
||||
func (d *dispatchSettings) Plugins() []string {
|
||||
r, e := callString(45, "", "", "", 0, 0); if e != nil || r == "" { return nil }; var v []string; json.Unmarshal([]byte(r), &v); return v
|
||||
}
|
||||
|
||||
// ---- Go callbacks (called from z_entry.c via C) ----
|
||||
|
||||
//export go_init_plugin
|
||||
func go_init_plugin(name *C.char, configJSON *C.char, errorOut **C.char) C.int {
|
||||
plg, err := NewPlugin(C.GoString(name), nil)
|
||||
if err != nil || plg == nil {
|
||||
if err != nil { *errorOut = C.CString(err.Error()) } else { *errorOut = C.CString("NewPlugin returned nil") }
|
||||
return 1
|
||||
}
|
||||
mu.Lock(); currentPlg = plg; mu.Unlock()
|
||||
_ = configJSON
|
||||
return 0
|
||||
}
|
||||
|
||||
//export go_start_plugin
|
||||
func go_start_plugin(coreAPIptr unsafe.Pointer, coreVersion C.int, errorOut **C.char) C.int {
|
||||
mu.Lock()
|
||||
plg := currentPlg
|
||||
coreAPI = coreAPIptr
|
||||
mu.Unlock()
|
||||
_ = coreVersion
|
||||
if plg == nil { *errorOut = C.CString("not initialized"); return 1 }
|
||||
sdk := buildPluginSDK(plg.Name())
|
||||
if err := plg.Start(sdk); err != nil { *errorOut = C.CString(err.Error()); return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
//export go_stop_plugin
|
||||
func go_stop_plugin(errorOut **C.char) C.int {
|
||||
mu.Lock()
|
||||
plg := currentPlg
|
||||
currentPlg = nil
|
||||
coreAPI = nil
|
||||
mu.Unlock()
|
||||
if plg != nil {
|
||||
if err := plg.Stop(); err != nil { *errorOut = C.CString(err.Error()); return 1 }
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
//export go_invoke_tool
|
||||
func go_invoke_tool(name *C.char, argsJSON *C.char, resultOut **C.char, errorOut **C.char) C.int {
|
||||
goName := C.GoString(name)
|
||||
handlerMu.RLock()
|
||||
h, ok := toolHandlers[goName]
|
||||
handlerMu.RUnlock()
|
||||
if !ok { *errorOut = C.CString("tool not found"); return 1 }
|
||||
var args map[string]interface{}
|
||||
if argsJSON != nil { json.Unmarshal([]byte(C.GoString(argsJSON)), &args) }
|
||||
r, err := h(args)
|
||||
if err != nil { *errorOut = C.CString(err.Error()); return 1 }
|
||||
b, _ := json.Marshal(r)
|
||||
*resultOut = C.CString(string(b))
|
||||
return 0
|
||||
}
|
||||
|
||||
//export go_invoke_stage
|
||||
func go_invoke_stage(stage *C.char, ctxJSON *C.char, errorOut **C.char) C.int {
|
||||
goStage := C.GoString(stage)
|
||||
handlerMu.RLock()
|
||||
h, ok := stageHandlers[goStage]
|
||||
handlerMu.RUnlock()
|
||||
if !ok { return 0 }
|
||||
sc := &sdk.StageContext{}
|
||||
if ctxJSON != nil {
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(C.GoString(ctxJSON)), &m); err == nil {
|
||||
if v, _ := m["raw_message"].(string); v != "" { sc.RawMessage = v }
|
||||
if v, _ := m["user_id"].(string); v != "" { sc.UserID = v }
|
||||
if v, _ := m["group_id"].(string); v != "" { sc.GroupID = v }
|
||||
if v, _ := m["phase"].(string); v != "" { sc.Phase = sdk.Stage(v) }
|
||||
if v, _ := m["llm_text"].(string); v != "" { sc.LLMText = v }
|
||||
if v, _ := m["final_text"].(string); v != "" { sc.FinalText = v }
|
||||
if v, _ := m["no_memory"].(bool); v { sc.NoMemory = true }
|
||||
if v, _ := m["response"].(string); v != "" { sc.Response = &v }
|
||||
if v, _ := m["tool_calls"].([]interface{}); len(v) > 0 {
|
||||
b, _ := json.Marshal(v); json.Unmarshal(b, &sc.ToolCalls)
|
||||
}
|
||||
if v, _ := m["tool_results"].([]interface{}); len(v) > 0 {
|
||||
b, _ := json.Marshal(v); json.Unmarshal(b, &sc.ToolResults)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := h(sc); err != nil { *errorOut = C.CString(err.Error()); return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
//export go_invoke_output
|
||||
func go_invoke_output(channel *C.char, msgType *C.char, payloadJSON *C.char, errorOut **C.char) C.int {
|
||||
goChan := C.GoString(channel)
|
||||
handlerMu.RLock()
|
||||
h, ok := outputHandlers[goChan]
|
||||
handlerMu.RUnlock()
|
||||
if !ok { return 0 }
|
||||
// payloadJSON contains the full args JSON from output_send (e.g. {"content":"...","user_id":123})
|
||||
var args map[string]interface{}
|
||||
if payloadJSON != nil {
|
||||
json.Unmarshal([]byte(C.GoString(payloadJSON)), &args)
|
||||
}
|
||||
if _, err := h(args); err != nil { *errorOut = C.CString(err.Error()); return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
//export go_free_string
|
||||
func go_free_string(ptr *C.char) { C.free(unsafe.Pointer(ptr)) }
|
||||
|
||||
func main() {}
|
||||
`
|
||||
|
||||
// tmplPluginInitC — C entry point for the plugin .so file.
|
||||
// Contains PluginAPI, CoreAPI (single dispatch), and ha_dispatch bridge.
|
||||
const tmplPluginInitC = `#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define HOMEAGENT_ABI_VERSION 1
|
||||
|
||||
typedef struct {
|
||||
int version; int version_min;
|
||||
int (*init_plugin)(char*, char*, char**);
|
||||
int (*start_plugin)(void*, int, char**);
|
||||
int (*stop_plugin)(char**);
|
||||
int (*invoke_tool)(char*, char*, char**, char**);
|
||||
int (*invoke_stage)(char*, char*, char**);
|
||||
int (*invoke_output)(char*, char*, char*, char**);
|
||||
void (*free_string)(char*);
|
||||
} PluginAPI;
|
||||
|
||||
typedef struct {
|
||||
int version; int version_min;
|
||||
int (*dispatch)(int, void*, char*, char*, char*, int, int, char**, char**);
|
||||
void* ctx;
|
||||
} CoreAPI;
|
||||
|
||||
extern int go_init_plugin(char*, char*, char**);
|
||||
extern int go_start_plugin(void*, int, char**);
|
||||
extern int go_stop_plugin(char**);
|
||||
extern int go_invoke_tool(char*, char*, char**, char**);
|
||||
extern int go_invoke_stage(char*, char*, char**);
|
||||
extern int go_invoke_output(char*, char*, char*, char**);
|
||||
extern void go_free_string(char*);
|
||||
|
||||
int c_init_plugin(char* n, char* c, char** e) { return go_init_plugin(n, c, e); }
|
||||
int c_start_plugin(void* a, int v, char** e) { return go_start_plugin(a, v, e); }
|
||||
int c_stop_plugin(char** e) { return go_stop_plugin(e); }
|
||||
int c_invoke_tool(char* n, char* a, char** r, char** e) { return go_invoke_tool(n, a, r, e); }
|
||||
int c_invoke_stage(char* s, char* c, char** e) { return go_invoke_stage(s, c, e); }
|
||||
int c_invoke_output(char* c, char* m, char* p, char** e) { return go_invoke_output(c, m, p, e); }
|
||||
void c_free_string(char* p) { go_free_string(p); }
|
||||
|
||||
// ha_dispatch — called by Go bridge, passes through to CoreAPI dispatch
|
||||
int ha_dispatch(int id, void* api, char* s1, char* s2, char* s3, int i1, int i2, char** r, char** e) {
|
||||
CoreAPI* a = (CoreAPI*)api;
|
||||
if (!a || !a->dispatch) return 1;
|
||||
return a->dispatch(id, a->ctx, s1, s2, s3, i1, i2, r, e);
|
||||
}
|
||||
|
||||
PluginAPI* plugin_init(void) {
|
||||
static PluginAPI api;
|
||||
memset(&api, 0, sizeof(api));
|
||||
api.version = HOMEAGENT_ABI_VERSION; api.version_min = HOMEAGENT_ABI_VERSION;
|
||||
api.init_plugin = c_init_plugin; api.start_plugin = c_start_plugin; api.stop_plugin = c_stop_plugin;
|
||||
api.invoke_tool = c_invoke_tool; api.invoke_stage = c_invoke_stage; api.invoke_output = c_invoke_output;
|
||||
api.free_string = c_free_string;
|
||||
return &api;
|
||||
}
|
||||
`
|
||||
|
||||
const tmplReadme = `# {{.Plg.Name}}
|
||||
|
||||
{{.Plg.Description}}
|
||||
|
||||
## Build
|
||||
|
||||
` + "```bash" + `
|
||||
plugindev build
|
||||
` + "```" + `
|
||||
|
||||
## Install
|
||||
|
||||
Upload the .hmap file through the Plugin Manager API.
|
||||
`
|
||||
1
tools/plugindev/templates.go.new_header
Normal file
1
tools/plugindev/templates.go.new_header
Normal file
@ -0,0 +1 @@
|
||||
I need to rewrite the preamble section. Let me use a python script to make this change.
|
||||
19
tools/plugindev/templates/README.md.tmpl
Normal file
19
tools/plugindev/templates/README.md.tmpl
Normal file
@ -0,0 +1,19 @@
|
||||
# {{.Plg.Name}}
|
||||
|
||||
{{.Plg.Description}}
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
plugindev build
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
Upload the `.hmap` file through the Plugin Manager API:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/v1/plugins \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary @dist/<name_en_snake>_linux_amd64.hmap
|
||||
```
|
||||
55
tools/plugindev/templates/main.go.tmpl
Normal file
55
tools/plugindev/templates/main.go.tmpl
Normal file
@ -0,0 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
)
|
||||
|
||||
type Plugin struct {
|
||||
name string
|
||||
sdk *sdk.PluginSDK
|
||||
}
|
||||
|
||||
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return &Plugin{name: name}, nil
|
||||
}
|
||||
|
||||
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.{{.Plg.Name}}.example",
|
||||
Default: "hello",
|
||||
Type: "string",
|
||||
DisplayName: "示例配置",
|
||||
Description: "An example configuration key",
|
||||
Category: "{{.Plg.Name}}",
|
||||
})
|
||||
|
||||
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 {{.Plg.Name}} plugin!",
|
||||
}, nil
|
||||
}
|
||||
45
tools/plugindev/templates/main.lua.tmpl
Normal file
45
tools/plugindev/templates/main.lua.tmpl
Normal file
@ -0,0 +1,45 @@
|
||||
local plugin = {
|
||||
name = "{{.Plg.Name}}",
|
||||
tools = {},
|
||||
stages = {},
|
||||
settings = {}
|
||||
}
|
||||
|
||||
function plugin:start(sdk)
|
||||
sdk.log("{{.Plg.Name}} plugin starting...")
|
||||
|
||||
-- Register a configuration setting
|
||||
-- sdk.settings.register({
|
||||
-- key = "{{.Plg.Name}}.example",
|
||||
-- default = "hello",
|
||||
-- type = "string",
|
||||
-- display_name = "Example Config",
|
||||
-- description = "An example configuration key"
|
||||
-- })
|
||||
|
||||
-- Register a tool
|
||||
local ok = sdk:register_tool("{{.Plg.Name}}_hello", {
|
||||
name = "{{.Plg.Name}}_hello",
|
||||
description = "A hello world tool",
|
||||
parameters = {
|
||||
type = "object",
|
||||
properties = {}
|
||||
}
|
||||
}, function(args)
|
||||
return { content = "Hello from {{.Plg.Name}} plugin!" }
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
sdk.log("error: failed to register tool")
|
||||
return false
|
||||
end
|
||||
|
||||
sdk.log("{{.Plg.Name}} plugin started")
|
||||
return true
|
||||
end
|
||||
|
||||
function plugin:stop()
|
||||
return true
|
||||
end
|
||||
|
||||
return plugin
|
||||
11
tools/plugindev/templates/plg.json.tmpl
Normal file
11
tools/plugindev/templates/plg.json.tmpl
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "{{.Plg.Name}}",
|
||||
"name_zh": "{{.Plg.NameZh}}",
|
||||
"name_en": "{{.Plg.NameEn}}",
|
||||
"version": "{{.Plg.Version}}",
|
||||
"description": "{{.Plg.Description}}",
|
||||
"author": "{{.Plg.Author}}",
|
||||
"entry": "{{.Plg.Entry}}",
|
||||
"tags": {{.Plg.Tags}},
|
||||
"targets": "{{.Plg.Targets}}"
|
||||
}
|
||||
13
tools/plugindev/testplugin/README.md
Normal file
13
tools/plugindev/testplugin/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# testplugin
|
||||
|
||||
testplugin plugin
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
plugindev build
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
Upload the .hmap file through the Plugin Manager API.
|
||||
7
tools/plugindev/testplugin/go.mod
Normal file
7
tools/plugindev/testplugin/go.mod
Normal file
@ -0,0 +1,7 @@
|
||||
module testplugin
|
||||
|
||||
go 1.21
|
||||
|
||||
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
|
||||
|
||||
replace gitcode.com/JianFeeeee/homeagent-sdk => E:/program/homeagent/homeagentsdk
|
||||
11
tools/plugindev/testplugin/main.go
Normal file
11
tools/plugindev/testplugin/main.go
Normal file
@ -0,0 +1,11 @@
|
||||
//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)
|
||||
}
|
||||
11
tools/plugindev/testplugin/plg.json
Normal file
11
tools/plugindev/testplugin/plg.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
57
tools/plugindev/testplugin/plugin.go
Normal file
57
tools/plugindev/testplugin/plugin.go
Normal file
@ -0,0 +1,57 @@
|
||||
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