diff --git a/homed b/homed new file mode 100755 index 0000000..7a5d264 Binary files /dev/null and b/homed differ diff --git a/internal/plugins/openclaw/plugin.go b/internal/plugins/openclaw/plugin.go index 2a99475..528c6a3 100644 --- a/internal/plugins/openclaw/plugin.go +++ b/internal/plugins/openclaw/plugin.go @@ -2,6 +2,7 @@ package openclaw import ( _ "embed" + "encoding/json" "fmt" "log" "os" @@ -73,12 +74,15 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { hasMainJS := false hasOCManifest := false + hasOCPackage := false for _, f := range subs { switch f.Name() { case "main.js": hasMainJS = true case "openclaw.plugin.json": hasOCManifest = true + case "package.json": + hasOCPackage = hasOCExtensions(filepath.Join(skillPath, "package.json")) } } @@ -87,7 +91,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { if err := p.loadSidecar(s, skillPath, entry.Name()); err != nil { log.Printf("[openclaw] sidecar %s: %v", entry.Name(), err) } - case hasOCManifest: + case hasOCManifest || hasOCPackage: if err := p.loadOCPlugin(s, skillPath, entry.Name()); err != nil { log.Printf("[openclaw] ocplugin %s: %v", entry.Name(), err) } @@ -221,3 +225,21 @@ func (p *Plugin) Stop() error { p.skills = nil return nil } + +// hasOCExtensions 检测 package.json 中是否有 openclaw.extensions 或 openclaw.runtimeExtensions +func hasOCExtensions(pkgPath string) bool { + data, err := os.ReadFile(pkgPath) + if err != nil { + return false + } + var pkg struct { + OpenClaw *struct { + Extensions interface{} `json:"extensions"` + RuntimeExtensions interface{} `json:"runtimeExtensions"` + } `json:"openclaw"` + } + if err := json.Unmarshal(data, &pkg); err != nil { + return false + } + return pkg.OpenClaw != nil && (pkg.OpenClaw.Extensions != nil || pkg.OpenClaw.RuntimeExtensions != nil) +} diff --git a/internal/plugins/openclaw/sidecar_test.go b/internal/plugins/openclaw/sidecar_test.go index c96b29f..f0d7969 100644 --- a/internal/plugins/openclaw/sidecar_test.go +++ b/internal/plugins/openclaw/sidecar_test.go @@ -5,6 +5,8 @@ import ( "os" "path/filepath" "testing" + + sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" ) func TestLaunchSidecarNoMainJS(t *testing.T) { @@ -188,3 +190,190 @@ func TestConcurrentCalls(t *testing.T) { <-done } } + +// ---- Simulator + OpenClaw plugin tests ---- + +func launchSimulator(t *testing.T, pluginDir, name string) *sidecarProcess { + t.Helper() + simPath, err := filepath.Abs(filepath.Join("simulator", "main.js")) + if err != nil { + t.Fatalf("abs simulator path: %v", err) + } + sp, err := launchProcess("node", simPath, pluginDir, name) + if err != nil { + t.Fatalf("launch simulator for %s: %v", name, err) + } + return sp +} + +func TestSimulatorWithOCSimple(t *testing.T) { + pluginDir, err := filepath.Abs(filepath.Join("testdata", "oc-simple")) + if err != nil { + t.Fatalf("abs testdata: %v", err) + } + sp := launchSimulator(t, pluginDir, "oc-simple") + defer sp.Close() + + tools, err := sp.ListTools() + if err != nil { + t.Fatalf("list tools: %v", err) + } + if len(tools) != 2 { + t.Fatalf("expected 2 tools, got %d: %+v", len(tools), tools) + } + + found := map[string]bool{"greet": false, "ping": false} + for _, tool := range tools { + found[tool.Name] = true + } + if !found["greet"] || !found["ping"] { + t.Fatalf("expected greet and ping tools, got %+v", tools) + } + + result, err := sp.CallTool("greet", map[string]interface{}{"name": "Test"}) + if err != nil { + t.Fatalf("call greet: %v", err) + } + if result != "Hello, Test!" { + t.Fatalf("expected 'Hello, Test!', got %q", result) + } +} + +func TestSimulatorWithOCPackage(t *testing.T) { + pluginDir, err := filepath.Abs(filepath.Join("testdata", "oc-pkg")) + if err != nil { + t.Fatalf("abs testdata: %v", err) + } + sp := launchSimulator(t, pluginDir, "oc-pkg") + defer sp.Close() + + tools, err := sp.ListTools() + if err != nil { + t.Fatalf("list tools: %v", err) + } + if len(tools) != 2 { + t.Fatalf("expected 2 tools, got %d: %+v", len(tools), tools) + } + + found := map[string]bool{"add": false, "info": false} + for _, tool := range tools { + found[tool.Name] = true + if tool.Name == "add" { + if tool.InputSchema == nil { + t.Error("add tool should have inputSchema") + } + } + } + if !found["add"] || !found["info"] { + t.Fatalf("expected add and info tools, got %+v", tools) + } + + result, err := sp.CallTool("add", map[string]interface{}{"a": 10.0, "b": 20.0}) + if err != nil { + t.Fatalf("call add: %v", err) + } + if result != "30" { + t.Fatalf("expected '30', got %q", result) + } + + infoResult, err := sp.CallTool("info", nil) + if err != nil { + t.Fatalf("call info: %v", err) + } + if infoResult == "" { + t.Fatal("expected non-empty info result") + } + t.Logf("info result: %s", infoResult) +} + +func TestLoadOCPluginViaPluginStart(t *testing.T) { + skillsDir := t.TempDir() + + ocSimpleDir := filepath.Join(skillsDir, "oc-simple") + if err := os.MkdirAll(ocSimpleDir, 0755); err != nil { + t.Fatalf("mkdir: %v", err) + } + for _, name := range []string{"openclaw.plugin.json", "index.js"} { + src := filepath.Join("testdata", "oc-simple", name) + data, err := os.ReadFile(src) + if err != nil { + t.Fatalf("read %s: %v", name, err) + } + if err := os.WriteFile(filepath.Join(ocSimpleDir, name), data, 0644); err != nil { + t.Fatalf("write %s: %v", name, err) + } + } + + ocPkgDir := filepath.Join(skillsDir, "oc-pkg") + if err := os.MkdirAll(filepath.Join(ocPkgDir, "lib"), 0755); err != nil { + t.Fatalf("mkdir: %v", err) + } + for _, name := range []string{"package.json", "lib/entry.js"} { + src := filepath.Join("testdata", "oc-pkg", name) + data, err := os.ReadFile(src) + if err != nil { + t.Fatalf("read %s: %v", name, err) + } + dst := filepath.Join(ocPkgDir, name) + if err := os.WriteFile(dst, data, 0644); err != nil { + t.Fatalf("write %s: %v", name, err) + } + } + + SimulatorDir = filepath.Join(t.TempDir(), ".simulator") + + p := New("openclaw", skillsDir) + + var registeredTools []string + registeredHandlers := make(map[string]sdk.ToolHandler) + sdk := sdk.New("openclaw", nil, nil, nil, nil, nil, nil, nil, nil, + func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error { + registeredTools = append(registeredTools, name) + registeredHandlers[name] = handler + return nil + }, + nil, nil) + + if err := p.Start(sdk); err != nil { + t.Fatalf("start plugin: %v", err) + } + defer p.Stop() + + expected := []string{"oc-simple_greet", "oc-simple_ping", "oc-pkg_add", "oc-pkg_info"} + for _, exp := range expected { + found := false + for _, name := range registeredTools { + if name == exp { + found = true + break + } + } + if !found { + t.Fatalf("expected tool %q not registered. got: %v", exp, registeredTools) + } + } + + handler, ok := registeredHandlers["oc-simple_greet"] + if !ok { + t.Fatal("greet handler not registered") + } + result, err := handler(map[string]interface{}{"name": "OpenClaw"}) + if err != nil { + t.Fatalf("exec greet: %v", err) + } + if result != "Hello, OpenClaw!" { + t.Fatalf("expected 'Hello, OpenClaw!', got %v", result) + } + + handler, ok = registeredHandlers["oc-pkg_add"] + if !ok { + t.Fatal("add handler not registered") + } + result, err = handler(map[string]interface{}{"a": 7.0, "b": 8.0}) + if err != nil { + t.Fatalf("exec add: %v", err) + } + if result != "15" { + t.Fatalf("expected '15', got %v", result) + } +} diff --git a/internal/plugins/openclaw/testdata/oc-pkg/lib/entry.js b/internal/plugins/openclaw/testdata/oc-pkg/lib/entry.js new file mode 100644 index 0000000..dc7d132 --- /dev/null +++ b/internal/plugins/openclaw/testdata/oc-pkg/lib/entry.js @@ -0,0 +1,33 @@ +module.exports = { + default: { + id: 'oc-pkg', + name: 'OC Package', + version: '2.0.0', + description: 'Test plugin discovered via package.json extensions', + register(api) { + api.registerTool({ + name: 'add', + description: 'Add two numbers', + parameters: { + type: 'object', + properties: { + a: { type: 'number', description: 'First number' }, + b: { type: 'number', description: 'Second number' }, + }, + required: ['a', 'b'], + }, + execute(id, params) { + return String(params.a + params.b); + }, + }); + + api.registerTool({ + name: 'info', + description: 'Return plugin info', + execute(id, params) { + return JSON.stringify({ id: this.id, name: this.name, version: this.version }); + }, + }); + }, + }, +}; diff --git a/internal/plugins/openclaw/testdata/oc-pkg/package.json b/internal/plugins/openclaw/testdata/oc-pkg/package.json new file mode 100644 index 0000000..4d748d8 --- /dev/null +++ b/internal/plugins/openclaw/testdata/oc-pkg/package.json @@ -0,0 +1,8 @@ +{ + "name": "oc-pkg", + "version": "2.0.0", + "description": "OC plugin with package.json entry discovery", + "openclaw": { + "extensions": ["lib/entry.js"] + } +} diff --git a/internal/plugins/openclaw/testdata/oc-simple/index.js b/internal/plugins/openclaw/testdata/oc-simple/index.js new file mode 100644 index 0000000..467bdd9 --- /dev/null +++ b/internal/plugins/openclaw/testdata/oc-simple/index.js @@ -0,0 +1,31 @@ +module.exports = { + default: { + id: 'oc-simple', + name: 'OC Simple', + version: '1.0.0', + description: 'Simple test plugin for OpenClaw simulator', + register(api) { + api.registerTool({ + name: 'greet', + description: 'Greet someone', + parameters: { + type: 'object', + properties: { + name: { type: 'string', description: 'Name to greet' }, + }, + required: ['name'], + }, + execute(id, params) { + return `Hello, ${params.name}!`; + }, + }); + api.registerTool({ + name: 'ping', + description: 'Health check ping', + execute(id, params) { + return 'pong'; + }, + }); + }, + }, +}; diff --git a/internal/plugins/openclaw/testdata/oc-simple/openclaw.plugin.json b/internal/plugins/openclaw/testdata/oc-simple/openclaw.plugin.json new file mode 100644 index 0000000..6204d3e --- /dev/null +++ b/internal/plugins/openclaw/testdata/oc-simple/openclaw.plugin.json @@ -0,0 +1,6 @@ +{ + "name": "oc-simple", + "version": "1.0.0", + "entry": "index.js", + "description": "Simple OpenClaw test plugin" +}