mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 18:08:04 +00:00
fix: cap distiller startup scan and switch to formal SDK module dependency
- replace vendored third_party/homeagent-sdk with formal module dependency - keep canonical SDK via go.mod pinned commit - optimize distiller startup loading to stream recent raw records only - parse timestamps correctly and cap startup load to 5000 records - remove quadratic string building in raw record parsing - restore fast startup while preserving memory pipeline
This commit is contained in:
@ -1,61 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# HomeAgent 插件打包工具
|
||||
# 将插件目录打包为 .hmap 分发包
|
||||
# 用法: ./packager.sh <plugin-dir> [输出路径]
|
||||
# 示例: ./packager.sh ./plugins/myplugin ./dist/myplugin-1.0.0.hmap
|
||||
|
||||
PLUGIN_DIR="${1:-}"
|
||||
OUTPUT="${2:-}"
|
||||
|
||||
if [ -z "$PLUGIN_DIR" ]; then
|
||||
echo "用法: $0 <plugin-dir> [输出路径]"
|
||||
echo "示例: $0 ./plugins/myplugin ./dist/myplugin-1.0.0.hmap"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PLUGIN_DIR="$(realpath "$PLUGIN_DIR")"
|
||||
PLUGIN_NAME="$(basename "$PLUGIN_DIR")"
|
||||
|
||||
# 验证
|
||||
if [ ! -f "$PLUGIN_DIR/plugin.json" ]; then
|
||||
echo "错误: 不存在 plugin.json: $PLUGIN_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="$(python3 -c "import json; print(json.load(open('$PLUGIN_DIR/plugin.json'))['version'])" 2>/dev/null || echo "unknown")"
|
||||
|
||||
if [ -z "$OUTPUT" ]; then
|
||||
mkdir -p dist
|
||||
OUTPUT="$(realpath "dist/${PLUGIN_NAME}-${VERSION}.hmap")"
|
||||
fi
|
||||
|
||||
echo "🔨 打包插件: $PLUGIN_NAME v$VERSION"
|
||||
echo " 源目录: $PLUGIN_DIR"
|
||||
echo " 输出: $OUTPUT"
|
||||
|
||||
# 检查入口文件
|
||||
ENTRY="$(python3 -c "import json; print(json.load(open('$PLUGIN_DIR/plugin.json'))['entry'])" 2>/dev/null || true)"
|
||||
if [ -n "$ENTRY" ] && [ ! -f "$PLUGIN_DIR/$ENTRY" ]; then
|
||||
echo "⚠️ 入口文件不存在: $ENTRY"
|
||||
echo " 请先编译: cd $PLUGIN_DIR && make"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查已编译的 .so
|
||||
if [ -f "$PLUGIN_DIR/plugin.so" ] && [ "$(stat -c %Y "$PLUGIN_DIR/plugin.so" 2>/dev/null)" -lt "$(stat -c %Y "$PLUGIN_DIR/plugin.go" 2>/dev/null)" ]; then
|
||||
echo "⚠️ plugin.so 比 plugin.go 旧,建议重新编译"
|
||||
echo " 请执行: cd $PLUGIN_DIR && make"
|
||||
fi
|
||||
|
||||
cd "$PLUGIN_DIR"
|
||||
zip -r "$OUTPUT" . -x "*.git*" "Makefile" ".gitignore" "*.go" "go.mod" "go.sum" "*.test" "testdata/*" "_*" 2>&1 | tail -3
|
||||
|
||||
echo ""
|
||||
echo "✅ 打包完成: $OUTPUT"
|
||||
echo " 大小: $(ls -lh "$OUTPUT" | awk '{print $5}')"
|
||||
echo ""
|
||||
echo "安装方式:"
|
||||
echo " 1. WebUI 插件管理 → 上传安装"
|
||||
echo " 2. AI 对话: 使用 plugin_install 工具并上传 URL"
|
||||
@ -1,45 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# HomeAgent 插件脚手架生成工具
|
||||
# 用法: ./scaffold.sh <plugin-name> [输出目录]
|
||||
# 示例: ./scaffold.sh myplugin ./plugins/myplugin
|
||||
|
||||
NAME="${1:-}"
|
||||
OUTDIR="${2:-./plugins/$NAME}"
|
||||
|
||||
if [ -z "$NAME" ]; then
|
||||
echo "用法: $0 <plugin-name> [输出目录]"
|
||||
echo "示例: $0 myplugin ./plugins/myplugin"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -d "$OUTDIR" ]; then
|
||||
echo "错误: 目标目录已存在: $OUTDIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
TEMPLATE_DIR="$SCRIPT_DIR/templates"
|
||||
|
||||
mkdir -p "$OUTDIR"
|
||||
|
||||
# 替换模板中的占位符
|
||||
sed -e "s/{{.Name}}/$NAME/g" \
|
||||
-e "s/{{.Version}}/0.1.0/g" \
|
||||
-e "s/{{.Description}}//g" \
|
||||
-e "s/{{.Author}}//g" \
|
||||
"$TEMPLATE_DIR/plugin.json.tmpl" > "$OUTDIR/plugin.json"
|
||||
|
||||
cp "$TEMPLATE_DIR/plugin.go.tmpl" "$OUTDIR/plugin.go"
|
||||
cp "$TEMPLATE_DIR/Makefile.tmpl" "$OUTDIR/Makefile"
|
||||
cp "$TEMPLATE_DIR/gitignore.tmpl" "$OUTDIR/.gitignore"
|
||||
|
||||
echo "✅ 插件脚手架已生成: $OUTDIR"
|
||||
echo ""
|
||||
echo "下一步:"
|
||||
echo " 1. 编辑 $OUTDIR/plugin.go 实现业务逻辑"
|
||||
echo " 2. 编辑 $OUTDIR/plugin.json 完善元信息"
|
||||
echo " 3. cd $OUTDIR && make # 编译 plugin.so"
|
||||
echo " 4. make package # 打包为 .hmap 分发包"
|
||||
echo " 5. 通过 WebUI 或 plugin_install 工具安装"
|
||||
@ -1,17 +0,0 @@
|
||||
# Build external Go plugin for HomeAgent
|
||||
# Usage: make # build plugin.so
|
||||
# make clean # remove plugin.so
|
||||
|
||||
PLUGIN_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
SDK_ROOT := $(realpath $(PLUGIN_DIR)../..)
|
||||
PLUGIN_NAME := $(notdir $(realpath $(PLUGIN_DIR)))
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: plugin.so
|
||||
|
||||
plugin.so:
|
||||
cd $(SDK_ROOT) && go build -buildmode=plugin -o $(PLUGIN_DIR)plugin.so $(PLUGIN_DIR)
|
||||
|
||||
clean:
|
||||
rm -f $(PLUGIN_DIR)plugin.so
|
||||
@ -1,2 +0,0 @@
|
||||
plugin.so
|
||||
*.hmap
|
||||
@ -1,54 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"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
|
||||
|
||||
tp := p.name + "_"
|
||||
|
||||
s.RegisterTool(tp+"example", sdk.ToolDef{
|
||||
Name: tp + "example",
|
||||
Description: "示例工具 - 请替换为实现",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"input": map[string]interface{}{"type": "string", "description": "输入参数"},
|
||||
},
|
||||
"required": []string{"input"},
|
||||
},
|
||||
}, p.handleExample)
|
||||
|
||||
log.Printf("[%s] plugin started", p.name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) handleExample(args map[string]interface{}) (interface{}, error) {
|
||||
input, _ := args["input"].(string)
|
||||
return map[string]interface{}{
|
||||
"echo": input,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func main() {}
|
||||
@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "{{.Name}}",
|
||||
"version": "{{.Version}}",
|
||||
"description": "{{.Description}}",
|
||||
"author": "{{.Author}}",
|
||||
"license": "MIT",
|
||||
"entry": "plugin.so",
|
||||
"min_version": "1.0.0",
|
||||
"tags": ["{{.Name}}"]
|
||||
}
|
||||
@ -1,237 +0,0 @@
|
||||
// Package plugintest provides a test harness for external HomeAgent plugins.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// import "gitcode.com/JianFeeeee/homeagent-sdk/hack/plugin-dev/testharness"
|
||||
//
|
||||
// func TestMyPlugin(t *testing.T) {
|
||||
// h := testharness.New(t, "./path/to/plugin.so")
|
||||
// defer h.Close()
|
||||
//
|
||||
// result, err := h.CallTool("myplugin_my_tool", map[string]interface{}{
|
||||
// "input": "hello",
|
||||
// })
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// t.Logf("result: %v", result)
|
||||
// }
|
||||
package plugintest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"plugin"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
)
|
||||
|
||||
// Harness is a test harness for loading and testing external Go plugins.
|
||||
type Harness struct {
|
||||
t *testing.T
|
||||
plug sdk.Plugin
|
||||
sdk *sdk.PluginSDK
|
||||
mu sync.Mutex
|
||||
tools map[string]sdk.ToolHandler
|
||||
stages map[sdk.Stage][]sdk.StageHandler
|
||||
setting *mockSettings
|
||||
}
|
||||
|
||||
// New loads a plugin .so and starts it with a mock SDK.
|
||||
// soPath is the path to the compiled plugin.so file.
|
||||
func New(t *testing.T, soPath string) *Harness {
|
||||
t.Helper()
|
||||
|
||||
absPath, err := filepath.Abs(soPath)
|
||||
if err != nil {
|
||||
t.Fatalf("abs path: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(absPath); err != nil {
|
||||
t.Fatalf("plugin not found: %s", absPath)
|
||||
}
|
||||
|
||||
pkg, err := plugin.Open(absPath)
|
||||
if err != nil {
|
||||
t.Fatalf("plugin.Open: %v", err)
|
||||
}
|
||||
|
||||
sym, err := pkg.Lookup("NewPlugin")
|
||||
if err != nil {
|
||||
t.Fatalf("NewPlugin symbol not found: %v", err)
|
||||
}
|
||||
newPlugin, ok := sym.(func(name string, config map[string]interface{}) (sdk.Plugin, error))
|
||||
if !ok {
|
||||
t.Fatal("NewPlugin has wrong signature")
|
||||
}
|
||||
|
||||
name := filepath.Base(filepath.Dir(absPath))
|
||||
plug, err := newPlugin(name, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("NewPlugin: %v", err)
|
||||
}
|
||||
|
||||
h := &Harness{
|
||||
t: t,
|
||||
plug: plug,
|
||||
tools: make(map[string]sdk.ToolHandler),
|
||||
stages: make(map[sdk.Stage][]sdk.StageHandler),
|
||||
setting: &mockSettings{
|
||||
data: make(map[string]interface{}),
|
||||
defs: make(map[string]sdk.ConfigDef),
|
||||
},
|
||||
}
|
||||
|
||||
h.sdk = sdk.New(name, h.setting, h.regTool, h.regStage, nil)
|
||||
|
||||
if err := plug.Start(h.sdk); err != nil {
|
||||
t.Fatalf("plugin.Start: %v", err)
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *Harness) regTool(name string, def sdk.ToolDef, handler sdk.ToolHandler) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.tools[name] = handler
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Harness) regStage(stage sdk.Stage, handler sdk.StageHandler) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.stages[stage] = append(h.stages[stage], handler)
|
||||
}
|
||||
|
||||
// Plug returns the loaded plugin instance.
|
||||
func (h *Harness) Plug() sdk.Plugin { return h.plug }
|
||||
|
||||
// SDK returns the mock PluginSDK.
|
||||
func (h *Harness) SDK() *sdk.PluginSDK { return h.sdk }
|
||||
|
||||
// Settings returns the mock settings store for test assertions.
|
||||
func (h *Harness) Settings() *mockSettings { return h.setting }
|
||||
|
||||
// ToolNames returns all registered tool names.
|
||||
func (h *Harness) ToolNames() []string {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
names := make([]string, 0, len(h.tools))
|
||||
for n := range h.tools {
|
||||
names = append(names, n)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// CallTool invokes a registered tool handler with the given arguments.
|
||||
func (h *Harness) CallTool(name string, args map[string]interface{}) (interface{}, error) {
|
||||
h.mu.Lock()
|
||||
handler, ok := h.tools[name]
|
||||
h.mu.Unlock()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("tool %q not registered", name)
|
||||
}
|
||||
return handler(args)
|
||||
}
|
||||
|
||||
// Close stops the plugin.
|
||||
func (h *Harness) Close() {
|
||||
if err := h.plug.Stop(); err != nil {
|
||||
h.t.Logf("plugin.Stop: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// AssertToolRegistered fails if the tool is not registered.
|
||||
func (h *Harness) AssertToolRegistered(name string) {
|
||||
h.t.Helper()
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if _, ok := h.tools[name]; !ok {
|
||||
h.t.Fatalf("expected tool %q to be registered", name)
|
||||
}
|
||||
}
|
||||
|
||||
// AssertToolResult checks that calling a tool returns the expected JSON output.
|
||||
func (h *Harness) AssertToolResult(name string, args map[string]interface{}, expected map[string]interface{}) {
|
||||
h.t.Helper()
|
||||
got, err := h.CallTool(name, args)
|
||||
if err != nil {
|
||||
h.t.Fatalf("tool %q: %v", name, err)
|
||||
}
|
||||
gotJSON, _ := json.Marshal(got)
|
||||
expJSON, _ := json.Marshal(expected)
|
||||
if string(gotJSON) != string(expJSON) {
|
||||
h.t.Fatalf("tool %q:\ngot: %s\nexp: %s", name, gotJSON, expJSON)
|
||||
}
|
||||
}
|
||||
|
||||
// mockSettings implements sdk.SettingsAPI for testing.
|
||||
type mockSettings struct {
|
||||
mu sync.Mutex
|
||||
data map[string]interface{}
|
||||
defs map[string]sdk.ConfigDef
|
||||
}
|
||||
|
||||
func (m *mockSettings) Get(key string) (interface{}, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
v, ok := m.data[key]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("key %q not found", key)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (m *mockSettings) Set(key string, value interface{}) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.data[key] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockSettings) List(prefix string) ([]string, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
var keys []string
|
||||
for k := range m.data {
|
||||
if prefix == "" || strings.HasPrefix(k, prefix) {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func (m *mockSettings) RegisterDef(def sdk.ConfigDef) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.defs[def.Key] = def
|
||||
}
|
||||
|
||||
func (m *mockSettings) Defs(prefix string) []*sdk.ConfigDef {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
var defs []*sdk.ConfigDef
|
||||
for _, d := range m.defs {
|
||||
if prefix == "" || strings.HasPrefix(d.Key, prefix) {
|
||||
defs = append(defs, &d)
|
||||
}
|
||||
}
|
||||
return defs
|
||||
}
|
||||
|
||||
func (m *mockSettings) Dump() map[string]interface{} {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
cp := make(map[string]interface{})
|
||||
for k, v := range m.data {
|
||||
cp[k] = v
|
||||
}
|
||||
return cp
|
||||
}
|
||||
|
||||
func (m *mockSettings) Plugins() []string { return nil }
|
||||
Reference in New Issue
Block a user