diff --git a/internal/plugin/bridge_e2e_test.go b/internal/plugin/bridge_e2e_test.go deleted file mode 100644 index f5969b1..0000000 --- a/internal/plugin/bridge_e2e_test.go +++ /dev/null @@ -1,262 +0,0 @@ -//go:build windows - -package plugin - -import ( - "encoding/json" - "os" - "path/filepath" - "syscall" - "testing" - "unsafe" - - sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" -) - -func TestBridgeE2E_WebPlugin(t *testing.T) { - exeDir, _ := os.Executable() - // Find web example build relative to the homeagent repo root - haRoot := findHomeAgentRoot(t, exeDir) - dllPath := filepath.Join(haRoot, "..", "homeagentsdk", "example", "web", "build", "plugin.dll") - if _, err := os.Stat(dllPath); os.IsNotExist(err) { - t.Fatalf("web plugin DLL not found at %s\nRun: cd example/web && plugindev build --target windows/amd64", dllPath) - } - - // Track captured tools and stages - var capturedTools []sdk.ToolDef - var capturedStages []sdk.Stage - - regTool := func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error { - capturedTools = append(capturedTools, def) - t.Logf(" registered tool: %s", name) - return nil - } - regStage := func(stage sdk.Stage, handler sdk.StageHandler) { - capturedStages = append(capturedStages, stage) - t.Logf(" registered stage: %s", stage) - } - regAPI := func(name string) error { - t.Logf(" registered API: %s", name) - return nil - } - - sett := sdk.NewSettings("web", nil) - psdk := sdk.New("web", sdk.SDKConfig{Settings: sett, RegTool: regTool, RegStage: regStage, RegAPI: regAPI}) - - plg, err := newDLLPlugin(dllPath, "web", nil) - if err != nil { - t.Fatalf("newDLLPlugin failed: %v", err) - } - defer plg.Stop() - - // Start — this calls NewPlugin + StartPlugin + registerTools + registerStages - if err := plg.Start(psdk); err != nil { - t.Fatalf("Start failed: %v", err) - } - - // Verify tools were captured - if len(capturedTools) == 0 { - t.Fatal("no tools were registered by web plugin") - } - t.Logf("Captured %d tools:", len(capturedTools)) - for _, d := range capturedTools { - t.Logf(" - %s: %s", d.Name, d.Description[:min(len(d.Description), 60)]) - } - - // Check specific expected tools - webSearch, webFetch := false, false - for _, d := range capturedTools { - if d.Name == "web_search" { - webSearch = true - if d.Description == "" { - t.Error("web_search has empty description") - } - params := d.Parameters - if params == nil { - t.Error("web_search has nil parameters") - } else { - if _, ok := params["properties"]; !ok { - t.Error("web_search parameters missing 'properties'") - } - } - } - if d.Name == "web_fetch" { - webFetch = true - } - } - if !webSearch { - t.Error("expected tool 'web_search' not registered") - } - if !webFetch { - t.Error("expected tool 'web_fetch' not registered") - } - - // Verify bridge exports work via direct C ABI calls - t.Logf("Bridge exports: getTools=%x invokeTool=%x freeCStr=%x", - plg.getTools, plg.invokeTool, plg.freeCStr) - - // GetToolDefsJSON - if plg.getTools != 0 { - toolDefsJSON := callGetToolDefsJSON(t, plg) - if len(toolDefsJSON) == 0 { - t.Error("GetToolDefsJSON returned empty array, expected tools") - } - for _, d := range toolDefsJSON { - t.Logf(" bridge tool: %s", d["name"]) - } - } - - // InvokeToolJSON — test with the search tool - if plg.invokeTool != 0 { - result := callInvokeToolJSON(t, plg, "web_search", map[string]interface{}{ - "query": "test", - "count": 1, - }) - t.Logf("InvokeToolJSON result keys: %v", keysOfMap(result)) - // Should get a result map (might be error if no network, but should not crash) - if errStr, ok := result["error"]; ok { - t.Logf(" (expected — tool returned error: %v)", errStr) - } - } -} - -func TestBridgeE2E_SanitizerStages(t *testing.T) { - exeDir, _ := os.Executable() - haRoot := findHomeAgentRoot(t, exeDir) - dllPath := filepath.Join(haRoot, "..", "homeagentsdk", "example", "sanitizer", "build", "plugin.dll") - if _, err := os.Stat(dllPath); os.IsNotExist(err) { - t.Skip("sanitizer DLL not built") - } - - var capturedStages []sdk.Stage - regStage := func(stage sdk.Stage, handler sdk.StageHandler) { - capturedStages = append(capturedStages, stage) - t.Logf(" registered stage: %s", stage) - } - - sett := sdk.NewSettings("sanitizer", nil) - psdk := sdk.New("sanitizer", sdk.SDKConfig{Settings: sett, - RegTool: func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error { return nil }, - RegStage: regStage, - RegAPI: func(name string) error { return nil }, - }) - - plg, err := newDLLPlugin(dllPath, "sanitizer", nil) - if err != nil { - t.Fatalf("newDLLPlugin failed: %v", err) - } - defer plg.Stop() - - if err := plg.Start(psdk); err != nil { - t.Fatalf("Start failed: %v", err) - } - - if len(capturedStages) == 0 { - t.Fatal("no stages registered by sanitizer") - } - found := false - for _, s := range capturedStages { - if s == sdk.StagePostAction { - found = true - } - } - if !found { - t.Fatalf("expected post_action stage, got %v", capturedStages) - } - - // Verify bridge GetStagesJSON - if plg.getStages != 0 { - ret, _, _ := syscall.SyscallN(plg.getStages, plg.handle) - if ret != 0 { - stagesJSON := cStringPtrToString(ret) - if plg.freeCStr != 0 { - syscall.SyscallN(plg.freeCStr, ret) - } - t.Logf("GetStagesJSON: %s", stagesJSON) - if !contains(t, stagesJSON, "post_action") { - t.Error("GetStagesJSON missing post_action") - } - } - } -} - -// --- helpers --- - -func findHomeAgentRoot(t *testing.T, exeDir string) string { - t.Helper() - // Walk up from test binary directory looking for homeagent/ - dir := exeDir - for i := 0; i < 10; i++ { - if _, err := os.Stat(filepath.Join(dir, "internal", "plugin")); err == nil { - return dir - } - parent := filepath.Dir(dir) - if parent == dir { - break - } - dir = parent - } - t.Fatal("cannot find homeagent root") - return "" -} - -func callGetToolDefsJSON(t *testing.T, plg *dllPlugin) []map[string]interface{} { - t.Helper() - ret, _, _ := syscall.SyscallN(plg.getTools, plg.handle) - if ret == 0 { - t.Fatal("GetToolDefsJSON returned nil") - } - jsonStr := cStringPtrToString(ret) - if plg.freeCStr != 0 { - syscall.SyscallN(plg.freeCStr, ret) - } - var defs []map[string]interface{} - if err := json.Unmarshal([]byte(jsonStr), &defs); err != nil { - t.Fatalf("GetToolDefsJSON parse error: %v", err) - } - return defs -} - -func callInvokeToolJSON(t *testing.T, plg *dllPlugin, toolName string, args map[string]interface{}) map[string]interface{} { - t.Helper() - argsJSON, _ := json.Marshal(args) - cToolName := append([]byte(toolName), 0) - cArgs := append(argsJSON, 0) - - ret, _, _ := syscall.SyscallN( - plg.invokeTool, - plg.handle, - uintptr(unsafe.Pointer(&cToolName[0])), - uintptr(unsafe.Pointer(&cArgs[0])), - ) - if ret == 0 { - t.Fatal("InvokeToolJSON returned nil") - } - jsonStr := cStringPtrToString(ret) - if plg.freeCStr != 0 { - syscall.SyscallN(plg.freeCStr, ret) - } - var result map[string]interface{} - if err := json.Unmarshal([]byte(jsonStr), &result); err != nil { - t.Fatalf("InvokeToolJSON parse error: %v (json=%s)", err, jsonStr) - } - return result -} - -func keysOfMap(m map[string]interface{}) []string { - var keys []string - for k := range m { - keys = append(keys, k) - } - return keys -} - -func contains(t *testing.T, s, substr string) bool { - t.Helper() - for i := 0; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return true - } - } - return false -} diff --git a/internal/plugin/cabi/loader.c b/internal/plugin/cabi/loader.c deleted file mode 100644 index bc16f07..0000000 --- a/internal/plugin/cabi/loader.c +++ /dev/null @@ -1,79 +0,0 @@ -//go:build linux || darwin - -// HomeAgent C ABI loader — C implementation (compiled alongside Go code via cgo) - -#include -#include -#include - -// HOMEAGENT_ABI_VERSION 与 internal/meta/meta.go CABINum 同步(major*100+minor,v0.9.x→900)。 -// C ABI 通过 version/version_min 协商,旧插件不受影响。 -#define HOMEAGENT_ABI_VERSION 900 - -// PluginAPI — provided by the plugin -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**, char**); - int (*invoke_output)(char*, char*, char*, char**); - void (*free_string)(char*); -} plugin_api_t; - -// CoreAPI — provided by the core -typedef struct { - int version; int version_min; - int (*dispatch)(int, void*, char*, char*, char*, int, int, char**, char**); - void* ctx; -} core_api_t; - -// Forward declare Go dispatch function -extern int go_core_dispatch(int, void*, char*, char*, char*, int, int, char**, char**); - -// Bridge function called by CoreAPI.dispatch -static int dispatch_bridge(int id, void* ctx, char* s1, char* s2, char* s3, int i1, int i2, char** r, char** e) { - return go_core_dispatch(id, ctx, s1, s2, s3, i1, i2, r, e); -} - -// Create a CoreAPI struct -core_api_t* make_core_api(void) { - core_api_t* api = (core_api_t*)malloc(sizeof(core_api_t)); - if (!api) return NULL; - api->version = HOMEAGENT_ABI_VERSION; - api->version_min = HOMEAGENT_ABI_VERSION; - api->dispatch = dispatch_bridge; - api->ctx = NULL; - return api; -} - -void free_core_api(core_api_t* api) { free(api); } - -// dlopen helpers -typedef void* lib_handle; - -lib_handle lib_open(const char* path) { - return dlopen(path, RTLD_NOW | RTLD_LOCAL); -} - -plugin_api_t* lib_get_api(lib_handle h) { - plugin_api_t* (*fn)(void); - *(void**)(&fn) = dlsym(h, "plugin_init"); - if (!fn) return NULL; - return fn(); -} - -void lib_close(lib_handle h) { dlclose(h); } -char* lib_err(void) { return dlerror(); } - -void api_free_string(plugin_api_t* api, char* ptr) { - if (api && api->free_string) api->free_string(ptr); -} - -int call_init_plugin(plugin_api_t* api, char* name, char* config, char** err) { return api->init_plugin(name, config, err); } -int call_start_plugin(plugin_api_t* api, void* core, int ver, char** err) { return api->start_plugin(core, ver, err); } -int call_stop_plugin(plugin_api_t* api, char** err) { return api->stop_plugin(err); } -int call_invoke_tool(plugin_api_t* api, char* n, char* a, char** r, char** e) { return api->invoke_tool(n, a, r, e); } -int call_invoke_stage(plugin_api_t* api, char* s, char* c, char** r, char** e) { return api->invoke_stage(s, c, r, e); } -int call_invoke_output(plugin_api_t* api, char* c, char* m, char* p, char** e) { return api->invoke_output(c, m, p, e); } diff --git a/internal/plugin/cabi/loader.go b/internal/plugin/cabi/loader.go deleted file mode 100644 index 6e666d5..0000000 --- a/internal/plugin/cabi/loader.go +++ /dev/null @@ -1,998 +0,0 @@ -//go:build linux || darwin - -package cabi - -/* -#cgo LDFLAGS: -ldl -#include - -// HOMEAGENT_ABI_VERSION 是当前内核的 C ABI 整数协商版本,由 internal/meta/meta.go CABINum 派生 -// (major*100 + minor,随核心版本号映射:v0.8.x→800,v0.9.x→900)。 -// 旧插件使用低整数版本不受影响——C ABI wrapper 通过 version/version_min 字段协商兼容。 -#define HOMEAGENT_ABI_VERSION 900 - -// PluginAPI — provided by the plugin via plugin_init() -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**, char**); - int (*invoke_output)(char*, char*, char*, char**); - void (*free_string)(char*); -} plugin_api_t; - -// CoreAPI — provided by the core via start_plugin() -typedef struct { - int version; int version_min; - int (*dispatch)(int, void*, char*, char*, char*, int, int, char**, char**); - void* ctx; -} core_api_t; - -// Functions implemented in loader.c -extern core_api_t* make_core_api(void); -extern void free_core_api(core_api_t* api); -extern int call_init_plugin(plugin_api_t*, char*, char*, char**); -extern int call_start_plugin(plugin_api_t*, void*, int, char**); -extern int call_stop_plugin(plugin_api_t*, char**); -extern int call_invoke_tool(plugin_api_t*, char*, char*, char**, char**); -extern int call_invoke_stage(plugin_api_t*, char*, char*, char**, char**); -extern int call_invoke_output(plugin_api_t*, char*, char*, char*, char**); -extern void api_free_string(plugin_api_t*, char*); -extern void* lib_open(const char*); -extern plugin_api_t* lib_get_api(void*); -extern void lib_close(void*); -extern char* lib_err(void); -*/ -import "C" -import ( - "encoding/json" - "fmt" - "log" - "sync" - "sync/atomic" - "time" - "unsafe" - - sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" -) - -// outputSendTimeout 是 output_send 等待通道真实发送确认的超时。 -// 超过该时间仍未收到插件确认,返回 unconfirmed(结果未知)而非谎报成功。 -// (plan.md 11.1) -const outputSendTimeout = 10 * time.Second - -var ( - pluginMap sync.Map // int32 pluginID → *pluginState - nextID int32 -) - -type pluginState struct { - id int32 - name string - sdk *sdk.PluginSDK - api *C.plugin_api_t -} - -// Handle represents a loaded C ABI plugin. -type Handle struct { - soPath string - lib unsafe.Pointer - api *C.plugin_api_t - core *C.core_api_t - pstate *pluginState -} - -// Load opens a .so plugin and initializes it via C ABI. -func Load(soPath, name string, config map[string]interface{}) (*Handle, error) { - cPath := C.CString(soPath) - defer C.free(unsafe.Pointer(cPath)) - - lib := C.lib_open(cPath) - if lib == nil { - return nil, fmt.Errorf("dlopen %s: %s", soPath, C.GoString(C.lib_err())) - } - - api := C.lib_get_api(lib) - if api == nil { - C.lib_close(lib) - return nil, fmt.Errorf("dlsym plugin_init in %s: %s", soPath, C.GoString(C.lib_err())) - } - if int(api.version) < 1 || api.init_plugin == nil { - C.lib_close(lib) - return nil, fmt.Errorf("plugin %s: invalid PluginAPI (version=%d)", name, int(api.version)) - } - if int(api.version) > CABINum { - C.lib_close(lib) - return nil, fmt.Errorf("plugin %s: ABI version %d > core %d (v%s), requires newer HomeAgent core", name, int(api.version), CABINum, ABIVersion) - } - - if int(api.version) < CABINumMin { - C.lib_close(lib) - return nil, fmt.Errorf("plugin %s: ABI version %d < core min %d (v%s), plugin too old", name, int(api.version), CABINumMin, ABIVersionMin) - } - - id := atomic.AddInt32(&nextID, 1) - ps := &pluginState{id: id, name: name, api: api} - pluginMap.Store(id, ps) - - handle := &Handle{soPath: soPath, lib: lib, api: api, pstate: ps} - - // Create CoreAPI later — done via CreateCoreAPI - - // Initialize plugin - configJSON, _ := json.Marshal(config) - cName := C.CString(name) - cConfig := C.CString(string(configJSON)) - var initErr *C.char - defer C.free(unsafe.Pointer(cName)) - defer C.free(unsafe.Pointer(cConfig)) - - if ret := int(C.call_init_plugin(api, cName, cConfig, &initErr)); ret != 0 { - errMsg := "" - if initErr != nil { - errMsg = C.GoString(initErr) - C.api_free_string(api, initErr) - } - handle.Close() - return nil, fmt.Errorf("init_plugin %s: %s", name, errMsg) - } - - return handle, nil -} - -// CreateCoreAPI creates a CoreAPI struct for this plugin. -// The CoreAPI dispatches all SDK calls back to Go, routing to the plugin's PluginSDK. -func (h *Handle) CreateCoreAPI(s *sdk.PluginSDK) unsafe.Pointer { - core := C.make_core_api() - if core == nil { - return nil - } - h.core = core - h.pstate.sdk = s - - // Store plugin ID as context (safe integer, not a Go pointer) - core.ctx = unsafe.Pointer(uintptr(h.pstate.id)) - - return unsafe.Pointer(core) -} - -// FreeCoreAPI frees the CoreAPI struct. -func (h *Handle) FreeCoreAPI() { - if h.core != nil { - C.free_core_api(h.core) - h.core = nil - } -} - -// Start calls the plugin's Start with a CoreAPI pointer. -func (h *Handle) Start(corePtr unsafe.Pointer) error { - var cErr *C.char - if ret := int(C.call_start_plugin(h.api, corePtr, C.int(CABINum), &cErr)); ret != 0 { - errMsg := "" - if cErr != nil { - errMsg = C.GoString(cErr) - C.api_free_string(h.api, cErr) - } - return fmt.Errorf("start_plugin: %s", errMsg) - } - return nil -} - -// Stop calls the plugin's Stop. -func (h *Handle) Stop() error { - var cErr *C.char - if ret := int(C.call_stop_plugin(h.api, &cErr)); ret != 0 { - errMsg := "" - if cErr != nil { - errMsg = C.GoString(cErr) - C.api_free_string(h.api, cErr) - } - return fmt.Errorf("stop_plugin: %s", errMsg) - } - return nil -} - -// InvokeTool calls a tool handler in the plugin. -func (h *Handle) InvokeTool(name string, args map[string]interface{}) (map[string]interface{}, error) { - argsJSON, _ := json.Marshal(args) - cName := C.CString(name) - cArgs := C.CString(string(argsJSON)) - var result, cErr *C.char - defer C.free(unsafe.Pointer(cName)) - defer C.free(unsafe.Pointer(cArgs)) - - if ret := int(C.call_invoke_tool(h.api, cName, cArgs, &result, &cErr)); ret != 0 { - errMsg := "" - if cErr != nil { - errMsg = C.GoString(cErr) - C.api_free_string(h.api, cErr) - } - return nil, fmt.Errorf("invoke_tool %s: %s", name, errMsg) - } - if result == nil { - return nil, nil - } - defer C.api_free_string(h.api, result) - var r map[string]interface{} - if err := json.Unmarshal([]byte(C.GoString(result)), &r); err != nil { - return nil, err - } - return r, nil -} - -// Close unloads the plugin library. -func (h *Handle) Close() { - if h.lib != nil { - C.lib_close(h.lib) - h.lib = nil - } -} - -// ---- plugin invocation helpers (stateless, use pluginMap lookup) ---- - -func pluginInvokeTool(pluginID int32, name, argsJSON string) (string, error) { - v, ok := pluginMap.Load(pluginID) - if !ok { - return "", fmt.Errorf("plugin %d not found", pluginID) - } - ps := v.(*pluginState) - if ps.api == nil { - return "", fmt.Errorf("plugin %d: nil api", pluginID) - } - cName := C.CString(name) - cArgs := C.CString(argsJSON) - var result, cErr *C.char - defer C.free(unsafe.Pointer(cName)) - defer C.free(unsafe.Pointer(cArgs)) - if ret := int(C.call_invoke_tool(ps.api, cName, cArgs, &result, &cErr)); ret != 0 { - errMsg := "" - if cErr != nil { - errMsg = C.GoString(cErr) - C.api_free_string(ps.api, cErr) - } - return "", fmt.Errorf("invoke_tool %s: %s", name, errMsg) - } - if result == nil { - return "", nil - } - defer C.api_free_string(ps.api, result) - return C.GoString(result), nil -} - -// awaitOutputResult 在 goroutine 内执行真正的 cgo 发送调用,并等待其结果: -// - 发送成功 → {status: sent} -// - 发送失败 → 返回 error(模型可感知并重试),不再像旧实现那样谎报成功 -// - 超时未确认 → {status: unconfirmed}(结果未知,不谎报成功/失败) -// -// 为什么用 goroutine + channel 而不是直接同步调用:pluginInvokeOutput 是 cgo 调用, -// 不能嵌套在 cgo 栈上执行(cgo within cgo 会崩溃)。本 handler 由 executeOutputSendTool -// 从 Go 侧调起(不在 cgo 栈内),所以这里启动子 goroutine 执行 cgo 调用并等待其结果, -// 不构成嵌套。 -// -// 修复 plan.md 11.1:旧实现无条件返回 {status: queued} + err=nil,模型永远收到「已发送」 -// 而实际失败(如 meta 缺 user_id)只写日志,模型无法感知、不会重试。 -func awaitOutputResult(pid int32, channel, argsJSON string) (interface{}, error) { - return awaitOutputResultWith(pid, channel, argsJSON, pluginInvokeOutput, outputSendTimeout) -} - -// awaitOutputResultWith 是 awaitOutputResult 的可注入版本(供单测替换 cgo 发送与超时)。 -func awaitOutputResultWith( - pid int32, - channel, argsJSON string, - invoke func(pluginID int32, channel, payload string) error, - timeout time.Duration, -) (interface{}, error) { - resCh := make(chan error, 1) - go func() { resCh <- invoke(pid, channel, argsJSON) }() - select { - case err := <-resCh: - if err != nil { - log.Printf("[dispatch] output %s failed: %v", channel, err) - return nil, err - } - log.Printf("[dispatch] output %s OK", channel) - return map[string]interface{}{"status": "sent"}, nil - case <-time.After(timeout): - // 超时未确认:插件仍在后台发送,结果未知。不谎报成功,也不谎报失败。 - log.Printf("[dispatch] output %s 等待确认超时(%s),插件仍在后台发送", channel, timeout) - return map[string]interface{}{ - "status": "unconfirmed", - "note": fmt.Sprintf("发送已提交但 %s 内未收到通道确认,结果未知;如需确认请查询该通道状态", timeout), - }, nil - } -} - -func pluginInvokeOutput(pluginID int32, channel, payload string) error { - v, ok := pluginMap.Load(pluginID) - if !ok { - return fmt.Errorf("plugin %d not found", pluginID) - } - ps := v.(*pluginState) - if ps.api == nil { - return fmt.Errorf("plugin %d: nil api", pluginID) - } - cCh := C.CString(channel) - cPayload := C.CString(payload) - var cErr *C.char - defer C.free(unsafe.Pointer(cCh)) - defer C.free(unsafe.Pointer(cPayload)) - if ret := int(C.call_invoke_output(ps.api, cCh, nil, cPayload, &cErr)); ret != 0 { - errMsg := "" - if cErr != nil { - errMsg = C.GoString(cErr) - C.api_free_string(ps.api, cErr) - } - return fmt.Errorf("invoke_output %s: %s", channel, errMsg) - } - return nil -} - -// applyStageResult 将插件回传的修改后上下文应用回内核 StageContext。 -// 只回写插件有权改写的字段(RawMessage/LLMText/FinalText/Response/ToolResults/NoMemory)。 -func applyStageResult(sc *sdk.StageContext, resultJSON string) { - var m map[string]interface{} - if err := json.Unmarshal([]byte(resultJSON), &m); err != nil { - return - } - sc.Lock() - defer sc.Unlock() - if v, ok := m["raw_message"].(string); ok { - sc.RawMessage = v - } - if v, ok := m["llm_text"].(string); ok { - sc.LLMText = v - } - if v, ok := m["final_text"].(string); ok { - sc.FinalText = v - } - if v, ok := m["user_id"].(string); ok { - sc.UserID = v - } - if v, ok := m["group_id"].(string); ok { - sc.GroupID = v - } - if v, ok := m["no_memory"].(bool); ok { - sc.NoMemory = v - } - if v, ok := m["response"].(string); ok { - vv := v - sc.Response = &vv - } - if v, ok := m["tool_calls"].([]interface{}); ok { - // 注意不要加 len(v)>0 条件:ABI v2 diff 回传(plan.md 11.3)下,插件拒绝全部 - // 工具调用时会显式回传 `[]`,必须能表达「清空」。旧插件(全量回传)仅在 - // len>0 时才带该键,因此不会因此变更而被误清空。 - if b, err := json.Marshal(v); err == nil { - var tcs []sdk.ToolCall - if json.Unmarshal(b, &tcs) == nil { - sc.ToolCalls = tcs - } - } - } - if v, ok := m["tool_results"].([]interface{}); ok { - if b, err := json.Marshal(v); err == nil { - var trs []sdk.ToolResult - if json.Unmarshal(b, &trs) == nil { - sc.ToolResults = trs - } - } - } -} - -func pluginInvokeStage(pluginID int32, stage, ctxJSON string, resultOut *string) error { - v, ok := pluginMap.Load(pluginID) - if !ok { - return fmt.Errorf("plugin %d not found", pluginID) - } - ps := v.(*pluginState) - if ps.api == nil { - return fmt.Errorf("plugin %d: nil api", pluginID) - } - cStage := C.CString(stage) - cCtx := C.CString(ctxJSON) - var cErr *C.char - var cResult *C.char - defer C.free(unsafe.Pointer(cStage)) - defer C.free(unsafe.Pointer(cCtx)) - // 仅当调用方要求回传时传 &cResult,否则传 NULL(兼容无需写回的阶段)。 - if ret := int(C.call_invoke_stage(ps.api, cStage, cCtx, &cResult, &cErr)); ret != 0 { - errMsg := "" - if cErr != nil { - errMsg = C.GoString(cErr) - C.api_free_string(ps.api, cErr) - } - return fmt.Errorf("invoke_stage %s: %s", stage, errMsg) - } - if resultOut != nil && cResult != nil { - *resultOut = C.GoString(cResult) - C.api_free_string(ps.api, cResult) - } - return nil -} - -// go_core_dispatch handles all plugin→core SDK calls. -// -//export go_core_dispatch -func go_core_dispatch(methodID C.int, ctx unsafe.Pointer, s1, s2, s3 *C.char, i1, i2 C.int, result **C.char, errorOut **C.char) C.int { - pluginID := int32(uintptr(ctx)) - v, ok := pluginMap.Load(pluginID) - if !ok { - return 1 - } - ps := v.(*pluginState) - s := ps.sdk - if s == nil { - return 1 - } - - a1, a2, a3 := goStr(s1), goStr(s2), goStr(s3) - n1, n2 := int(i1), int(i2) - - switch int(methodID) { - case 1: // CORE_REGISTER_TOOL - var def sdk.ToolDef - if err := json.Unmarshal([]byte(a2), &def); err != nil { - setErr(errorOut, err) - return 1 - } - def.Plugin = ps.name - pid := pluginID - toolName := a1 - _ = s.RegisterTool(a1, def, func(args map[string]interface{}) (interface{}, error) { - argsJSON, _ := json.Marshal(args) - r, err := pluginInvokeTool(pid, toolName, string(argsJSON)) - if err != nil { - return nil, err - } - if r == "" { - return nil, nil - } - var res map[string]interface{} - if err := json.Unmarshal([]byte(r), &res); err != nil { - return r, nil - } - return res, nil - }) - return 0 - - case 2: // CORE_REGISTER_STAGE - pid := pluginID - st := a1 - handler := func(sc *sdk.StageContext) error { - sc.RLock() - m := map[string]interface{}{ - "raw_message": sc.RawMessage, "user_id": sc.UserID, - "group_id": sc.GroupID, "phase": string(sc.Phase), - "llm_text": sc.LLMText, "final_text": sc.FinalText, - "no_memory": sc.NoMemory, - } - if sc.Response != nil { - m["response"] = *sc.Response - } - if len(sc.ToolCalls) > 0 { - m["tool_calls"] = sc.ToolCalls - } - if len(sc.ToolResults) > 0 { - m["tool_results"] = sc.ToolResults - } - sc.RUnlock() - b, _ := json.Marshal(m) - - // ABI v2: 插件可回传修改后的上下文写回内核 sc(如 RawMessage/LLMText/Response/ToolResults)。 - var result string - if err := pluginInvokeStage(pid, st, string(b), &result); err != nil { - return err - } - if result != "" { - applyStageResult(sc, result) - } - return nil - } - scope := sdk.StageScopeGlobal - if a3 == "own_tools" { - scope = sdk.StageScopeOwnTools - } - s.RegisterStage(sdk.Stage(st), handler, scope) - return 0 - - case 3: // CORE_REGISTER_OUTPUT_CH - pid := pluginID - chName := a1 - chDef := sdk.ChannelDef{} - if a3 != "" { - var def sdk.ChannelDef - if err := json.Unmarshal([]byte(a3), &def); err == nil { - chDef = def - } - } - s.RegisterOutputChannel(chName, n1, a2, chDef, func(args map[string]interface{}) (interface{}, error) { - // 发送在 goroutine 内进行(cgo 调用不能嵌套在 cgo 栈上,否则可能崩溃), - // 但调用方必须拿到真实结果:本 handler 由 executeOutputSendTool 从 Go 侧 - // 调起,不在 cgo 栈内,因此这里等待 goroutine 的结果不构成 cgo 嵌套。 - // (plan.md 11.1) - argsJSON, _ := json.Marshal(args) - log.Printf("[dispatch] output %s/%s args=%s", ps.name, chName, string(argsJSON)) - return awaitOutputResult(pid, chName, string(argsJSON)) - }) - return 0 - - case 4: // CORE_REGISTER_PLUGIN_API - s.RegisterPluginAPI(a1) - return 0 - - case 5: // CORE_INJECT_TEXT - s.InjectText(a1, a2, a3) - return 0 - - case 6: // CORE_INJECT_INTERRUPT_TEXT - s.InjectInterruptText(a1, a2, a3) - return 0 - - case 7: // CORE_INJECT_TEXT_NO_MEMORY - s.InjectTextNoMemory(a1, a2, a3) - return 0 - - case 47: // CORE_INJECT_INPUT_SYNC - if out := s.InjectInputSync(a1, a2, "text", map[string]interface{}{"content": a3}); out != nil { - reply, _ := out.Payload["content"].(string) - setResult(result, reply) - } - return 0 - - case 8: // CORE_SET_AUTO_RESTART - s.SetAutoRestart(n1 != 0) - return 0 - - case 9: // CORE_MEMORY_RECALL - if mem := s.Memory(); mem != nil { - entities, relations, err := mem.Recall([]string{a1}, n1) - if err != nil { - setErr(errorOut, err) - return 1 - } - b, _ := json.Marshal(map[string]interface{}{"entities": entities, "relations": relations}) - setResult(result, string(b)) - } - return 0 - - case 10: // CORE_MEMORY_COMMIT - if mem := s.Memory(); mem != nil { - var triples []sdk.Triple - if err := json.Unmarshal([]byte(a1), &triples); err != nil { - setErr(errorOut, err) - return 1 - } - if err := mem.Commit(triples); err != nil { - setErr(errorOut, err) - return 1 - } - } - return 0 - - case 11: // CORE_MEMORY_INTROSPECT - if mem := s.Memory(); mem != nil { - r, err := mem.Introspect() - if err != nil { - setErr(errorOut, err) - return 1 - } - b, _ := json.Marshal(r) - setResult(result, string(b)) - } - return 0 - - case 12: // CORE_MEMORY_MERGE - if mem := s.Memory(); mem != nil { - if _, err := mem.MergeEntities(a1, a2); err != nil { - setErr(errorOut, err) - return 1 - } - } - return 0 - - case 13: // CORE_MEMORY_PURGE - if mem := s.Memory(); mem != nil { - var criteria map[string]string - if err := json.Unmarshal([]byte(a1), &criteria); err != nil { - setErr(errorOut, err) - return 1 - } - mode := "soft" - if n1 != 0 { - mode = "hard" - } - if _, err := mem.Purge(criteria, mode); err != nil { - setErr(errorOut, err) - return 1 - } - } - return 0 - - case 14: // CORE_DOC_QUERY - if dm := s.DocMemory(); dm != nil { - b, _ := json.Marshal(dm.Query(a1, n1)) - setResult(result, string(b)) - } - return 0 - - case 15: // CORE_KNOWLEDGE_SEARCH - if kn := s.Knowledge(); kn != nil { - results, err := kn.Search(a1, n1) - if err != nil { - setErr(errorOut, err) - return 1 - } - b, _ := json.Marshal(results) - setResult(result, string(b)) - } - return 0 - - case 16: // CORE_SETTINGS_GET - if sett := s.Settings(); sett != nil { - v, err := sett.Get(a1) - if err != nil { - setErr(errorOut, err) - return 1 - } - b, _ := json.Marshal(v) - setResult(result, string(b)) - } - return 0 - - case 17: // CORE_SETTINGS_SET - if sett := s.Settings(); sett != nil { - var v interface{} - json.Unmarshal([]byte(a2), &v) - if err := sett.Set(a1, v); err != nil { - setErr(errorOut, err) - return 1 - } - } - return 0 - - case 18: // CORE_SETTINGS_REGISTER_DEF - if sett := s.Settings(); sett != nil { - var def sdk.ConfigDef - if err := json.Unmarshal([]byte(a1), &def); err != nil { - setErr(errorOut, err) - return 1 - } - sett.RegisterDef(def) - } - return 0 - - case 19: // CORE_LLM_LIST_SOURCES - if llm := s.LLM(); llm != nil { - b, _ := json.Marshal(llm.ListSources()) - setResult(result, string(b)) - } - return 0 - - case 20: // CORE_LLM_SET_SOURCE - if llm := s.LLM(); llm != nil { - if err := llm.SetSource(a1); err != nil { - setErr(errorOut, err) - return 1 - } - } - return 0 - - case 21: // CORE_SOCIAL_GET_PERSON - if social := s.Social(); social != nil { - p, err := social.GetPerson(a1) - if err != nil { - setErr(errorOut, err) - return 1 - } - b, _ := json.Marshal(p) - setResult(result, string(b)) - } - return 0 - - case 22: // CORE_SOCIAL_GET_NETWORK - if social := s.Social(); social != nil { - profiles, err := social.GetNetwork(a1, n1) - if err != nil { - setErr(errorOut, err) - return 1 - } - b, _ := json.Marshal(profiles) - setResult(result, string(b)) - } - return 0 - - case 23: // CORE_SUBSCRIBE - _ = n2 - // Events API not wired for external plugins (SetEventSubscriber not called) - return 0 - - case 24: // CORE_UNSUBSCRIBE - return 0 - - case 25: // CORE_FREE_STRING - if s1 != nil { - C.free(unsafe.Pointer(s1)) - } - return 0 - - case 26: // CORE_SETTINGS_GET_CORE - if sett := s.Settings(); sett != nil { - v, err := sett.GetCore(a1) - if err != nil { - setErr(errorOut, err) - return 1 - } - b, _ := json.Marshal(v) - setResult(result, string(b)) - } - return 0 - - case 27: // CORE_SETTINGS_SET_CORE - if sett := s.Settings(); sett != nil { - var v interface{} - json.Unmarshal([]byte(a2), &v) - if err := sett.SetCore(a1, v); err != nil { - setErr(errorOut, err) - return 1 - } - } - return 0 - - case 28: // CORE_SETTINGS_LIST_CORE - if sett := s.Settings(); sett != nil { - keys, err := sett.ListCore(a1) - if err != nil { - setErr(errorOut, err) - return 1 - } - b, _ := json.Marshal(keys) - setResult(result, string(b)) - } - return 0 - - case 29: // CORE_SETTINGS_GET_PLUGIN - if sett := s.Settings(); sett != nil { - v, err := sett.GetPlugin(a1, a2) - if err != nil { - setErr(errorOut, err) - return 1 - } - b, _ := json.Marshal(v) - setResult(result, string(b)) - } - return 0 - - case 30: // CORE_SETTINGS_SET_PLUGIN - if sett := s.Settings(); sett != nil { - var v interface{} - json.Unmarshal([]byte(a3), &v) - if err := sett.SetPlugin(a1, a2, v); err != nil { - setErr(errorOut, err) - return 1 - } - } - return 0 - - case 31: // CORE_SETTINGS_LIST_PLUGIN - if sett := s.Settings(); sett != nil { - keys, err := sett.ListPlugin(a1, a2) - if err != nil { - setErr(errorOut, err) - return 1 - } - b, _ := json.Marshal(keys) - setResult(result, string(b)) - } - return 0 - - case 32: // CORE_DOC_INSERT - if dm := s.DocMemory(); dm != nil { - var doc sdk.Doc - if err := json.Unmarshal([]byte(a1), &doc); err != nil { - setErr(errorOut, err) - return 1 - } - if err := dm.Insert(&doc); err != nil { - setErr(errorOut, err) - return 1 - } - } - return 0 - - case 33: // CORE_DOC_REMOVE - if dm := s.DocMemory(); dm != nil { - dm.Remove(a1) - } - return 0 - - case 34: // CORE_DOC_STATS - if dm := s.DocMemory(); dm != nil { - b, _ := json.Marshal(dm.Stats()) - setResult(result, string(b)) - } - return 0 - - case 35: // CORE_KNOWLEDGE_ADD - if kn := s.Knowledge(); kn != nil { - if err := kn.Add(a1, a2); err != nil { - setErr(errorOut, err) - return 1 - } - } - return 0 - - case 36: // CORE_KNOWLEDGE_LIST - if kn := s.Knowledge(); kn != nil { - list, err := kn.List() - if err != nil { - setErr(errorOut, err) - return 1 - } - b, _ := json.Marshal(list) - setResult(result, string(b)) - } - return 0 - - case 37: // CORE_LLM_CURRENT_SOURCE - if llm := s.LLM(); llm != nil { - b, _ := json.Marshal(llm.CurrentSource()) - setResult(result, string(b)) - } - return 0 - - case 38: // CORE_SOCIAL_GET_TRAIT - if social := s.Social(); social != nil { - val, ok := social.GetTrait(a1, a2) - b, _ := json.Marshal(map[string]interface{}{"value": val, "found": ok}) - setResult(result, string(b)) - } - return 0 - - case 39: // CORE_SOCIAL_GET_RELATIONS - if social := s.Social(); social != nil { - rels, err := social.GetRelations(a1) - if err != nil { - setErr(errorOut, err) - return 1 - } - b, _ := json.Marshal(rels) - setResult(result, string(b)) - } - return 0 - - case 40: // CORE_SOCIAL_LIST_PERSONS - if social := s.Social(); social != nil { - persons, err := social.ListPersons() - if err != nil { - setErr(errorOut, err) - return 1 - } - b, _ := json.Marshal(persons) - setResult(result, string(b)) - } - return 0 - - case 41: // CORE_TEXT_MEMORY_APPEND - if tm := s.TextMemory(); tm != nil { - var evt sdk.TextEvent - if err := json.Unmarshal([]byte(a1), &evt); err != nil { - setErr(errorOut, err) - return 1 - } - if err := tm.Append(evt); err != nil { - setErr(errorOut, err) - return 1 - } - } - return 0 - - case 42: // CORE_SETTINGS_LIST - if sett := s.Settings(); sett != nil { - keys, err := sett.List(a1) - if err != nil { - setErr(errorOut, err) - return 1 - } - b, _ := json.Marshal(keys) - setResult(result, string(b)) - } - return 0 - - case 43: // CORE_SETTINGS_DEFS - if sett := s.Settings(); sett != nil { - defs := sett.Defs(a1) - b, _ := json.Marshal(defs) - setResult(result, string(b)) - } - return 0 - - case 44: // CORE_SETTINGS_DUMP - if sett := s.Settings(); sett != nil { - dump := sett.Dump() - b, _ := json.Marshal(dump) - setResult(result, string(b)) - } - return 0 - - case 45: // CORE_SETTINGS_PLUGINS - if sett := s.Settings(); sett != nil { - plugins := sett.Plugins() - b, _ := json.Marshal(plugins) - setResult(result, string(b)) - } - return 0 - - case 51: // CORE_SETTINGS_DATA_DIR:插件专属数据目录(内核保证存在) - if sett := s.Settings(); sett != nil { - setResult(result, sett.DataDir()) - } - return 0 - - case 46: // CORE_REGISTER_INPUT_CH - chDef := sdk.ChannelDef{} - if a2 != "" { - var def sdk.ChannelDef - if err := json.Unmarshal([]byte(a2), &def); err == nil { - chDef = def - } - } - s.RegisterInputChannel(a1, chDef) - return 0 - - case 48: // CORE_PLUGIN_RELOAD_ONE - if s.PluginMgr() == nil { - setErr(errorOut, fmt.Errorf("plugin manager not available")) - return 1 - } - if err := s.PluginMgr().ReloadOne(a1); err != nil { - setErr(errorOut, err) - return 1 - } - setResult(result, "reloaded: "+a1) - return 0 - - case 49: // CORE_PLUGIN_LIST_LOADED - if s.PluginMgr() == nil { - setErr(errorOut, fmt.Errorf("plugin manager not available")) - return 1 - } - if b, err := json.Marshal(s.PluginMgr().ListLoadedPlugins()); err == nil { - setResult(result, string(b)) - } - return 0 - - case 50: // CORE_PLUGIN_IS_DISABLED - if s.PluginMgr() == nil { - setErr(errorOut, fmt.Errorf("plugin manager not available")) - return 1 - } - if s.PluginMgr().IsPluginDisabled(a1) { - setResult(result, "1") - } else { - setResult(result, "0") - } - return 0 - } - return 0 -} - -func goStr(s *C.char) string { - if s == nil { - return "" - } - return C.GoString(s) -} - -func setErr(errOut **C.char, err error) { - if errOut != nil && err != nil { - *errOut = C.CString(err.Error()) - } -} - -func setResult(result **C.char, v string) { - if result != nil { - *result = C.CString(v) - } -} diff --git a/internal/plugin/cabi/output_test.go b/internal/plugin/cabi/output_test.go deleted file mode 100644 index 2c54437..0000000 --- a/internal/plugin/cabi/output_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package cabi - -import ( - "errors" - "strings" - "testing" - "time" - - sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" -) - -// output_send 不再假成功(plan.md 11.1):sent / error / unconfirmed 三态。 - -func TestAwaitOutputResult_Success(t *testing.T) { - res, err := awaitOutputResultWith(0, "qq", `{"x":1}`, func(pid int32, ch, args string) error { - return nil - }, outputSendTimeout) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - m, _ := res.(map[string]interface{}) - if m["status"] != "sent" { - t.Fatalf("expected status=sent, got %v", m["status"]) - } -} - -func TestAwaitOutputResult_Failure(t *testing.T) { - _, err := awaitOutputResultWith(0, "qq", `{}`, func(pid int32, ch, args string) error { - return errors.New("meta 中需要 group_id 或 user_id 字段") - }, outputSendTimeout) - if err == nil { - t.Fatal("expected error on failed send, got nil (旧实现会谎报成功)") - } - if !strings.Contains(err.Error(), "需要 group_id") { - t.Fatalf("unexpected error: %v", err) - } -} - -func TestAwaitOutputResult_Timeout(t *testing.T) { - res, err := awaitOutputResultWith(0, "qq", `{}`, func(pid int32, ch, args string) error { - time.Sleep(2 * time.Second) // 模拟插件发送迟迟不确认 - return nil - }, 50*time.Millisecond) - if err != nil { - t.Fatalf("unconfirmed 不应返回 error,got %v", err) - } - m, _ := res.(map[string]interface{}) - if m["status"] != "unconfirmed" { - t.Fatalf("expected status=unconfirmed, got %v", m["status"]) - } -} - -// applyStageResult 必须能表达「插件清空了 tool_calls/tool_results」—— -// ABI v2 diff 回传(plan.md 11.3)下插件拒绝全部工具调用时会显式回传 []。 -func TestApplyStageResult_ClearedSlicesAreApplied(t *testing.T) { - sc := &sdk.StageContext{ - ToolCalls: []sdk.ToolCall{{ID: "t1", Name: "cmd_run"}}, - ToolResults: []sdk.ToolResult{{CallID: "t1", Name: "cmd_run", Result: "x"}}, - } - applyStageResult(sc, `{"tool_calls":[],"tool_results":[]}`) - if len(sc.ToolCalls) != 0 { - t.Fatalf("tool_calls 应被清空,实际 %v", sc.ToolCalls) - } - if len(sc.ToolResults) != 0 { - t.Fatalf("tool_results 应被清空,实际 %v", sc.ToolResults) - } -} - -// diff 回传只带变更字段:未出现的键不得被改动(避免旧快照覆盖)。 -func TestApplyStageResult_OnlyPresentKeysApplied(t *testing.T) { - sc := &sdk.StageContext{ - RawMessage: "原始输入", - LLMText: "原始LLM", - FinalText: "原始最终", - ToolResults: []sdk.ToolResult{{CallID: "c1", Result: "已清洗"}}, - } - // 只回传 final_text 的变更 - applyStageResult(sc, `{"final_text":"新最终"}`) - - if sc.FinalText != "新最终" { - t.Fatalf("final_text 应被应用,实际 %q", sc.FinalText) - } - if sc.RawMessage != "原始输入" { - t.Errorf("raw_message 未回传却被改动: %q", sc.RawMessage) - } - if sc.LLMText != "原始LLM" { - t.Errorf("llm_text 未回传却被改动: %q", sc.LLMText) - } - if len(sc.ToolResults) != 1 || sc.ToolResults[0].Result != "已清洗" { - t.Errorf("tool_results 未回传却被改动: %v", sc.ToolResults) - } -} diff --git a/internal/plugin/cabi/types.go b/internal/plugin/cabi/types.go deleted file mode 100644 index 764f0a3..0000000 --- a/internal/plugin/cabi/types.go +++ /dev/null @@ -1,66 +0,0 @@ -package cabi - -import "gitcode.com/JianFeeeee/HomeAgent/internal/meta" - -// ABI version constants — single source of truth is meta.go -// ABIVersion/ABIVersionMin 是字符串 semver(var 转发,因 meta 侧 Version 为注入变量); -// CABINum/CABINumMin 是 C 层整数协商版本。 -var ( - ABIVersion = meta.ABIVersion - ABIVersionMin = meta.ABIVersionMin -) - -const ( - CABINum = meta.CABINum - CABINumMin = meta.CABINumMin -) - -// Dispatch method IDs — single source of truth is meta.go -const ( - CoreRegisterTool = meta.CoreRegisterTool - CoreRegisterStage = meta.CoreRegisterStage - CoreRegisterOutputCh = meta.CoreRegisterOutputCh - CoreRegisterPluginAPI = meta.CoreRegisterPluginAPI - CoreInjectText = meta.CoreInjectText - CoreInjectInterruptText = meta.CoreInjectInterruptText - CoreInjectTextNoMemory = meta.CoreInjectTextNoMemory - CoreSetAutoRestart = meta.CoreSetAutoRestart - CoreMemoryRecall = meta.CoreMemoryRecall - CoreMemoryCommit = meta.CoreMemoryCommit - CoreMemoryIntrospect = meta.CoreMemoryIntrospect - CoreMemoryMerge = meta.CoreMemoryMerge - CoreMemoryPurge = meta.CoreMemoryPurge - CoreDocQuery = meta.CoreDocQuery - CoreKnowledgeSearch = meta.CoreKnowledgeSearch - CoreSettingsGet = meta.CoreSettingsGet - CoreSettingsSet = meta.CoreSettingsSet - CoreSettingsRegisterDef = meta.CoreSettingsRegisterDef - CoreLLMListSources = meta.CoreLLMListSources - CoreLLMSetSource = meta.CoreLLMSetSource - CoreSocialGetPerson = meta.CoreSocialGetPerson - CoreSocialGetNetwork = meta.CoreSocialGetNetwork - CoreSubscribe = meta.CoreSubscribe - CoreUnsubscribe = meta.CoreUnsubscribe - CoreFreeString = meta.CoreFreeString - CoreSettingsGetCore = meta.CoreSettingsGetCore - CoreSettingsSetCore = meta.CoreSettingsSetCore - CoreSettingsListCore = meta.CoreSettingsListCore - CoreSettingsGetPlugin = meta.CoreSettingsGetPlugin - CoreSettingsSetPlugin = meta.CoreSettingsSetPlugin - CoreSettingsListPlugin = meta.CoreSettingsListPlugin - CoreDocInsert = meta.CoreDocInsert - CoreDocRemove = meta.CoreDocRemove - CoreDocStats = meta.CoreDocStats - CoreKnowledgeAdd = meta.CoreKnowledgeAdd - CoreKnowledgeList = meta.CoreKnowledgeList - CoreLLMCurrentSource = meta.CoreLLMCurrentSource - CoreSocialGetTrait = meta.CoreSocialGetTrait - CoreSocialGetRelations = meta.CoreSocialGetRelations - CoreSocialListPersons = meta.CoreSocialListPersons - CoreTextMemoryAppend = meta.CoreTextMemoryAppend - CoreSettingsList = meta.CoreSettingsList - CoreSettingsDefs = meta.CoreSettingsDefs - CoreSettingsDump = meta.CoreSettingsDump - CoreSettingsPlugins = meta.CoreSettingsPlugins - CoreRegisterInputCh = meta.CoreRegisterInputCh -) diff --git a/internal/plugin/dynamic.go b/internal/plugin/dynamic.go index 33aecc0..b583051 100644 --- a/internal/plugin/dynamic.go +++ b/internal/plugin/dynamic.go @@ -7,31 +7,34 @@ import ( ) const ( - soEntry = "plugin.so" - dllEntry = "plugin.dll" - binEntry = "plugin.bin" // 子进程插件(纯 Go 二进制,stdio JSON-RPC) + binEntry = "plugin.bin" // 子进程插件(纯 Go 二进制,stdio JSON-RPC + 共享内存) luaEntry = "main.lua" skillEntry = "SKILL.md" metaEntry = "plugin.json" ) +// legacyCABIEntries 是已退场的 C ABI 产物名。 +// +// 保留这张表只为**给出明确错误**:插件目录里躺着 plugin.so 而内核不再认它时, +// 静默跳过会让「目录在但插件没加载」看起来像配置问题,而实际原因是需要用 +// 新版 plugindev 重编。 +var legacyCABIEntries = []string{"plugin.so", "plugin.dll", "plugin.dylib"} + // entryKind 描述插件入口归属的加载通道。 -// 外部插件多进程化期间 .so/.dll(cabi)与 .bin(proc)**双通道共存**, -// 按 plugin.json 的 entry 字段分派,使迁移可逐插件推进、随时回退。 +// +// C ABI 通道(.so/.dll/.dylib)已整体退场:外部插件统一走子进程 + stdio RPC, +// 三套独立 ABI 实现收敛为单一 RPC 实现(§9.2)。 type entryKind int const ( entryUnknown entryKind = iota - entryCABI // plugin.so / plugin.dll / plugin.dylib —— C ABI 动态库 - entryProc // plugin.bin —— 子进程 + stdio JSON-RPC + entryProc // plugin.bin —— 子进程 + stdio JSON-RPC + 共享内存 entryLua // main.lua entrySkill // SKILL.md ) func (k entryKind) String() string { switch k { - case entryCABI: - return "cabi" case entryProc: return "proc" case entryLua: @@ -43,11 +46,12 @@ func (k entryKind) String() string { } // classifyEntry 把 manifest 的 entry 字段映射到加载通道。 -// entry 为空时返回 entryUnknown,由调用方回退到目录探测(兼容无 manifest 的旧插件)。 +// +// entry 为空或声明已退场的 C ABI 产物时返回 entryUnknown, +// 由调用方回退到目录探测(兼容无 manifest 的旧插件), +// 并在探测到 C ABI 残留时给出明确的重编提示。 func classifyEntry(entry string) entryKind { switch entry { - case soEntry, dllEntry, "plugin.dylib": - return entryCABI case binEntry: return entryProc case luaEntry: @@ -59,8 +63,9 @@ func classifyEntry(entry string) entryKind { } // detectEntryKind 先读 manifest 的 entry,读不到则按目录内存在的入口文件推断。 -// 推断顺序:.bin 优先于 .so——迁移期间同一插件目录可能两个产物共存(升级未清理), -// 此时应走新通道;manifest 显式声明优先级最高。 +// +// 注意:存量插件的 plugin.json 可能仍写着 "plugin.so"(工具链已不再据此分派, +// 但历史产物里有),此时 classifyEntry 返回 unknown,靠目录探测找到 plugin.bin。 func detectEntryKind(plgDir string) entryKind { if mft := readManifest(plgDir); mft != nil { if k := classifyEntry(mft.Entry); k != entryUnknown { @@ -72,9 +77,6 @@ func detectEntryKind(plgDir string) entryKind { kind entryKind }{ {binEntry, entryProc}, - {soEntry, entryCABI}, - {"plugin.dylib", entryCABI}, - {dllEntry, entryCABI}, {luaEntry, entryLua}, {skillEntry, entrySkill}, } { @@ -85,6 +87,18 @@ func detectEntryKind(plgDir string) entryKind { return entryUnknown } +// hasLegacyCABIEntry 判断插件目录里是否只剩已退场的 C ABI 产物。 +// +// 用于给出「需要重编」而非「插件不存在」的错误。 +func hasLegacyCABIEntry(plgDir string) bool { + for _, name := range legacyCABIEntries { + if st, err := os.Stat(filepath.Join(plgDir, name)); err == nil && !st.IsDir() { + return true + } + } + return false +} + func readManifest(dir string) *PluginManifest { data, err := os.ReadFile(filepath.Join(dir, metaEntry)) if err != nil { @@ -96,5 +110,3 @@ func readManifest(dir string) *PluginManifest { } return &m } - -var _ = json.Marshal diff --git a/internal/plugin/dynamic_dll_stub.go b/internal/plugin/dynamic_dll_stub.go deleted file mode 100644 index 5fbcae6..0000000 --- a/internal/plugin/dynamic_dll_stub.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build !windows - -package plugin - -import ( - sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" -) - -func tryLoadDLL(dir, name string, config map[string]interface{}) (sdk.Plugin, error) { - return nil, nil -} diff --git a/internal/plugin/dynamic_dll_test.go b/internal/plugin/dynamic_dll_test.go deleted file mode 100644 index a97f822..0000000 --- a/internal/plugin/dynamic_dll_test.go +++ /dev/null @@ -1,32 +0,0 @@ -//go:build windows - -package plugin - -import ( - "os" - "path/filepath" - "testing" -) - -func TestTryLoadDLL_NoFile(t *testing.T) { - dir := t.TempDir() - plg, err := tryLoadDLL(dir, "nonexistent", nil) - if err != nil { - t.Fatalf("tryLoadDLL on empty dir should not error: %v", err) - } - if plg != nil { - t.Fatal("expected nil for non-existent plugin.dll") - } -} - -func TestTryLoadDLL_Invalid(t *testing.T) { - dir := t.TempDir() - os.WriteFile(filepath.Join(dir, "plugin.dll"), []byte("not a real dll"), 0644) - - plg, err := tryLoadDLL(dir, "baddll", nil) - t.Logf("plg=%v err=%v", plg, err) - - if err == nil && plg == nil { - t.Fatal("expected error or non-nil plugin for existing file") - } -} diff --git a/internal/plugin/dynamic_dll_windows.go b/internal/plugin/dynamic_dll_windows.go deleted file mode 100644 index 4b73b72..0000000 --- a/internal/plugin/dynamic_dll_windows.go +++ /dev/null @@ -1,272 +0,0 @@ -//go:build windows - -package plugin - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - "syscall" - "unsafe" - - sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" -) - -// dllPlugin wraps a Windows DLL compiled with -buildmode=c-shared. -// -// Required exports: -// -// NewPlugin(name *C.char, configJSON *C.char) unsafe.Pointer → plugin handle -// StartPlugin(handle unsafe.Pointer) C.int -// StopPlugin(handle unsafe.Pointer) C.int -// DestroyPlugin(handle unsafe.Pointer) -// -// Optional exports (tool registration): -// -// GetToolDefsJSON(handle unsafe.Pointer) *C.char → JSON array of tool defs -// InvokeToolJSON(handle unsafe.Pointer, toolName *C.char, argsJSON *C.char) *C.char -// FreeCString(s *C.char) → free C string from DLL -// GetStagesJSON(handle unsafe.Pointer) *C.char → JSON array of stage names -// InvokeStage(handle unsafe.Pointer, stage *C.char, contextJSON *C.char) C.int -type dllPlugin struct { - name string - dll syscall.Handle - handle uintptr - sdk *sdk.PluginSDK - - // cached proc addresses - newPlugin uintptr - startPlugin uintptr - stopPlugin uintptr - destroyPlugin uintptr - getTools uintptr - invokeTool uintptr - freeCStr uintptr - getStages uintptr - invokeStage uintptr -} - -func findProc(dll syscall.Handle, name string) uintptr { - addr, err := syscall.GetProcAddress(dll, name) - if err != nil { - return 0 - } - return addr -} - -func newDLLPlugin(dllPath, name string, config map[string]interface{}) (*dllPlugin, error) { - dll, err := syscall.LoadLibrary(dllPath) - if err != nil { - return nil, fmt.Errorf("LoadLibrary %s: %w", dllPath, err) - } - - np := findProc(dll, "NewPlugin") - if np == 0 { - _ = syscall.FreeLibrary(dll) - return nil, fmt.Errorf("dll %s must export NewPlugin", name) - } - - return &dllPlugin{ - name: name, - dll: dll, - // required - newPlugin: np, - startPlugin: findProc(dll, "StartPlugin"), - stopPlugin: findProc(dll, "StopPlugin"), - destroyPlugin: findProc(dll, "DestroyPlugin"), - // optional tool/stage API - getTools: findProc(dll, "GetToolDefsJSON"), - invokeTool: findProc(dll, "InvokeToolJSON"), - freeCStr: findProc(dll, "FreeCString"), - getStages: findProc(dll, "GetStagesJSON"), - invokeStage: findProc(dll, "InvokeStage"), - }, nil -} - -func (p *dllPlugin) Name() string { return p.name } - -func (p *dllPlugin) Start(s *sdk.PluginSDK) error { - p.sdk = s - - cfgJSON, _ := json.Marshal(map[string]interface{}{ - "name": p.name, - "config": s.Settings().Dump(), - }) - cName := append([]byte(p.name), 0) - cConfig := append(cfgJSON, 0) - - ret, _, _ := syscall.SyscallN( - p.newPlugin, - uintptr(unsafe.Pointer(&cName[0])), - uintptr(unsafe.Pointer(&cConfig[0])), - ) - if ret == 0 { - _ = syscall.FreeLibrary(p.dll) - return fmt.Errorf("dll NewPlugin %s returned nil", p.name) - } - p.handle = ret - - if p.startPlugin != 0 { - syscall.SyscallN(p.startPlugin, p.handle) - } - - // discover and register tools from DLL - if p.getTools != 0 { - if err := p.registerTools(s); err != nil { - return fmt.Errorf("dll %s register tools: %w", p.name, err) - } - } - if p.getStages != 0 { - p.registerStages(s) - } - return nil -} - -func (p *dllPlugin) Stop() error { - if p.stopPlugin != 0 { - syscall.SyscallN(p.stopPlugin, p.handle) - } - if p.destroyPlugin != 0 { - syscall.SyscallN(p.destroyPlugin, p.handle) - } - _ = syscall.FreeLibrary(p.dll) - return nil -} - -// --- tool registration via C ABI --- - -type dllToolDef struct { - Name string `json:"name"` - Description string `json:"description"` - Parameters map[string]interface{} `json:"parameters,omitempty"` -} - -func (p *dllPlugin) registerTools(s *sdk.PluginSDK) error { - ret, _, _ := syscall.SyscallN(p.getTools, p.handle) - if ret == 0 { - return nil // no tools - } - defsJSON := cStringPtrToString(ret) - if p.freeCStr != 0 { - syscall.SyscallN(p.freeCStr, ret) - } - - var defs []dllToolDef - if err := json.Unmarshal([]byte(defsJSON), &defs); err != nil { - return fmt.Errorf("parse tool defs: %w", err) - } - for _, d := range defs { - if d.Name == "" { - continue - } - toolName := d.Name - handler := p.makeToolHandler(toolName) - s.RegisterTool(toolName, sdk.ToolDef{ - Name: toolName, - Description: d.Description, - Parameters: d.Parameters, - Plugin: p.name, - }, handler) - } - return nil -} - -func (p *dllPlugin) makeToolHandler(toolName string) sdk.ToolHandler { - return func(args map[string]interface{}) (interface{}, error) { - if p.invokeTool == 0 { - return nil, fmt.Errorf("dll %s does not export InvokeToolJSON", p.name) - } - argsJSON, _ := json.Marshal(args) - cToolName := append([]byte(toolName), 0) - cArgs := append(argsJSON, 0) - - ret, _, _ := syscall.SyscallN( - p.invokeTool, - p.handle, - uintptr(unsafe.Pointer(&cToolName[0])), - uintptr(unsafe.Pointer(&cArgs[0])), - ) - if ret == 0 { - return nil, fmt.Errorf("dll InvokeToolJSON %s returned nil", toolName) - } - resultJSON := cStringPtrToString(ret) - if p.freeCStr != 0 { - syscall.SyscallN(p.freeCStr, ret) - } - var result map[string]interface{} - if err := json.Unmarshal([]byte(resultJSON), &result); err != nil { - return nil, fmt.Errorf("dll tool %s result parse: %w", toolName, err) - } - return result, nil - } -} - -func (p *dllPlugin) registerStages(s *sdk.PluginSDK) { - ret, _, _ := syscall.SyscallN(p.getStages, p.handle) - if ret == 0 { - return - } - stagesJSON := cStringPtrToString(ret) - if p.freeCStr != 0 { - syscall.SyscallN(p.freeCStr, ret) - } - type stageEntry struct { - Stage string `json:"stage"` - } - var entries []stageEntry - if err := json.Unmarshal([]byte(stagesJSON), &entries); err != nil { - return - } - for _, e := range entries { - if e.Stage == "" { - continue - } - stageName := sdk.Stage(e.Stage) - stage := stageName - s.RegisterStage(stage, func(sc *sdk.StageContext) error { - if p.invokeStage == 0 { - return nil - } - ctxJSON, _ := json.Marshal(map[string]interface{}{ - "raw_message": sc.RawMessage, - "user_id": sc.UserID, - "phase": string(sc.Phase), - }) - cStage := append([]byte(stage), 0) - cCtx := append(ctxJSON, 0) - syscall.SyscallN( - p.invokeStage, - p.handle, - uintptr(unsafe.Pointer(&cStage[0])), - uintptr(unsafe.Pointer(&cCtx[0])), - ) - return nil - }) - } -} - -func cStringPtrToString(ptr uintptr) string { - if ptr == 0 { - return "" - } - var buf []byte - for i := uintptr(0); ; i++ { - b := *(*byte)(unsafe.Pointer(ptr + i)) - if b == 0 { - break - } - buf = append(buf, b) - } - return string(buf) -} - -// tryLoadDLL 尝试从插件目录加载 plugin.dll。 -// 返回 nil,nil 表示目录中没有 plugin.dll。 -func tryLoadDLL(dir, name string, config map[string]interface{}) (sdk.Plugin, error) { - dllPath := filepath.Join(dir, dllEntry) - if _, err := os.Stat(dllPath); os.IsNotExist(err) { - return nil, nil - } - return newDLLPlugin(dllPath, name, config) -} diff --git a/internal/plugin/dynamic_loader_unix.go b/internal/plugin/dynamic_loader_unix.go deleted file mode 100644 index da96944..0000000 --- a/internal/plugin/dynamic_loader_unix.go +++ /dev/null @@ -1,79 +0,0 @@ -//go:build linux || darwin - -package plugin - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - - pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk" - sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" - - "gitcode.com/JianFeeeee/HomeAgent/internal/plugin/cabi" -) - -type dynamicPlugin struct { - name string - impl pubsdk.Plugin -} - -func (p *dynamicPlugin) Name() string { return p.name } -func (p *dynamicPlugin) Start(s *sdk.PluginSDK) error { - return p.impl.Start(s.PluginSDK) -} -func (p *dynamicPlugin) Stop() error { return p.impl.Stop() } - -type cabiPlugin struct { - name string - handle *cabi.Handle -} - -func (p *cabiPlugin) Name() string { return p.name } -func (p *cabiPlugin) Start(s *sdk.PluginSDK) error { - corePtr := p.handle.CreateCoreAPI(s) - if corePtr == nil { - return fmt.Errorf("cabi: failed to create CoreAPI for %s", p.name) - } - if err := p.handle.Start(corePtr); err != nil { - return fmt.Errorf("cabi: start %s: %w", p.name, err) - } - return nil -} - -func (p *cabiPlugin) Stop() error { - _ = p.handle.Stop() - return nil -} - -// Close 卸载动态库(dlclose)。卸载/重载后必须调用,否则同一路径的 dlopen -// 会复用旧句柄(Linux dlopen 语义),新版本的 plugin.so 不会生效。 -func (p *cabiPlugin) Close() error { - p.handle.Close() - return nil -} - -func tryLoadSO(dir, name string, config map[string]interface{}) (sdk.Plugin, error) { - soPath := filepath.Join(dir, soEntry) - if _, err := os.Stat(soPath); os.IsNotExist(err) { - // 回退尝试 plugin.dylib (macOS 原生扩展名) - dylibPath := filepath.Join(dir, "plugin.dylib") - if _, err2 := os.Stat(dylibPath); err2 == nil { - soPath = dylibPath - } else { - return nil, nil - } - } - - handle, err := cabi.Load(soPath, name, config) - if err == nil { - return &cabiPlugin{name: name, handle: handle}, nil - } - // 本项目插件统一由 plugindev 编译为 c-shared 走 C ABI; - // 对 c-shared .so 调用 Go plugin.Open 会 fatal(no plugin module data), - // 因此不再 fallback 到 Go plugin,直接返回加载错误避免崩溃。 - return nil, err -} - -var _ = json.Marshal diff --git a/internal/plugin/dynamic_loader_windows.go b/internal/plugin/dynamic_loader_windows.go deleted file mode 100644 index c7db6cf..0000000 --- a/internal/plugin/dynamic_loader_windows.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build windows - -package plugin - -import ( - sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" -) - -func tryLoadSO(dir, name string, config map[string]interface{}) (sdk.Plugin, error) { - return nil, nil -} diff --git a/internal/plugin/entry_dispatch_test.go b/internal/plugin/entry_dispatch_test.go index ced5aec..c19066f 100644 --- a/internal/plugin/entry_dispatch_test.go +++ b/internal/plugin/entry_dispatch_test.go @@ -3,26 +3,30 @@ package plugin import ( "os" "path/filepath" + "strings" "testing" ) -// entry 分派骨架(docs/zh/plugin-migration-plan.md Part 1): -// 外部插件多进程化期间 .so/.dll(cabi)与 .bin(proc)双通道共存, -// 按 plugin.json 的 entry 字段分派,使迁移可逐插件推进、随时回退。 +// entry 分派(docs/zh/plugin-migration-plan.md Part 1/6)。 +// +// C ABI 通道(.so/.dll/.dylib)已整体退场:外部插件统一走子进程 + stdio RPC。 +// 这些测试守住的是「旧产物给明确错误」而非「静默跳过」——后者会让 +// 「插件目录在但没加载」看起来像配置问题。 func TestClassifyEntry(t *testing.T) { cases := []struct { entry string want entryKind }{ - {"plugin.so", entryCABI}, - {"plugin.dll", entryCABI}, - {"plugin.dylib", entryCABI}, {"plugin.bin", entryProc}, {"main.lua", entryLua}, {"SKILL.md", entrySkill}, {"", entryUnknown}, {"plugin.wasm", entryUnknown}, + // 已退场的 C ABI 产物不再是有效通道 + {"plugin.so", entryUnknown}, + {"plugin.dll", entryUnknown}, + {"plugin.dylib", entryUnknown}, } for _, c := range cases { if got := classifyEntry(c.entry); got != c.want { @@ -34,45 +38,34 @@ func TestClassifyEntry(t *testing.T) { // manifest 显式声明的 entry 优先级最高。 func TestDetectEntryKind_ManifestWins(t *testing.T) { dir := t.TempDir() - // 目录里放 .so,但 manifest 声明 .bin → 应走 proc - mustWrite(t, filepath.Join(dir, "plugin.so"), "fake so") + mustWrite(t, filepath.Join(dir, "main.lua"), "fake lua") mustWrite(t, filepath.Join(dir, "plugin.bin"), "fake bin") - mustWrite(t, filepath.Join(dir, metaEntry), `{"name":"x","entry":"plugin.bin"}`) + mustWrite(t, filepath.Join(dir, metaEntry), `{"name":"x","entry":"main.lua"}`) - if got := detectEntryKind(dir); got != entryProc { - t.Fatalf("manifest 声明 plugin.bin 应走 proc,实际 %v", got) + if got := detectEntryKind(dir); got != entryLua { + t.Fatalf("manifest 声明 main.lua 应走 lua,实际 %v", got) } } -// manifest 声明 .so 时即便存在 .bin 也走 cabi —— 这是回退路径的保证。 -func TestDetectEntryKind_ManifestCanForceRollback(t *testing.T) { +// 存量插件的 plugin.json 仍写着 "plugin.so"(历史产物), +// 此时 classifyEntry 返回 unknown,须靠目录探测找到 plugin.bin。 +// +// 这是「外部插件零改动」的直接后果:17 个插件的 manifest 没人去改。 +func TestDetectEntryKind_LegacyManifestFallsBackToProbe(t *testing.T) { dir := t.TempDir() - mustWrite(t, filepath.Join(dir, "plugin.so"), "fake so") mustWrite(t, filepath.Join(dir, "plugin.bin"), "fake bin") mustWrite(t, filepath.Join(dir, metaEntry), `{"name":"x","entry":"plugin.so"}`) - if got := detectEntryKind(dir); got != entryCABI { - t.Fatalf("manifest 声明 plugin.so 应回退到 cabi,实际 %v", got) - } -} - -// 无 manifest(或 entry 为空)时按目录探测,.bin 优先于 .so: -// 迁移期间同目录可能两种产物共存(升级未清理),此时应走新通道。 -func TestDetectEntryKind_ProbeOrderPrefersBin(t *testing.T) { - dir := t.TempDir() - mustWrite(t, filepath.Join(dir, "plugin.so"), "fake so") - mustWrite(t, filepath.Join(dir, "plugin.bin"), "fake bin") - if got := detectEntryKind(dir); got != entryProc { - t.Fatalf("无 manifest 时应优先 plugin.bin,实际 %v", got) + t.Fatalf("manifest 写 plugin.so 但目录有 plugin.bin 时应走 proc,实际 %v", got) } } func TestDetectEntryKind_ProbeFallbacks(t *testing.T) { - t.Run("only so", func(t *testing.T) { + t.Run("only bin", func(t *testing.T) { dir := t.TempDir() - mustWrite(t, filepath.Join(dir, "plugin.so"), "x") - if got := detectEntryKind(dir); got != entryCABI { + mustWrite(t, filepath.Join(dir, "plugin.bin"), "x") + if got := detectEntryKind(dir); got != entryProc { t.Fatalf("got %v", got) } }) @@ -95,15 +88,60 @@ func TestDetectEntryKind_ProbeFallbacks(t *testing.T) { t.Fatalf("空目录应为 unknown,实际 %v", got) } }) + t.Run("only legacy so", func(t *testing.T) { + dir := t.TempDir() + mustWrite(t, filepath.Join(dir, "plugin.so"), "x") + if got := detectEntryKind(dir); got != entryUnknown { + t.Fatalf("只有 .so 时应为 unknown(C ABI 已退场),实际 %v", got) + } + }) } -// entry 声明 plugin.bin 但二进制缺失时必须报明确错误, -// 不得静默回退到 cabi —— 否则"已迁移插件跑回旧通道"极难排查。 +// C ABI 残留必须能被识别,供 tryDynamic 给出「需要重编」的明确错误。 +func TestHasLegacyCABIEntry(t *testing.T) { + for _, name := range []string{"plugin.so", "plugin.dll", "plugin.dylib"} { + dir := t.TempDir() + mustWrite(t, filepath.Join(dir, name), "x") + if !hasLegacyCABIEntry(dir) { + t.Errorf("%s 应被识别为 C ABI 残留", name) + } + } + t.Run("clean dir", func(t *testing.T) { + dir := t.TempDir() + mustWrite(t, filepath.Join(dir, "plugin.bin"), "x") + if hasLegacyCABIEntry(dir) { + t.Error("只有 plugin.bin 的目录不应被判为 C ABI 残留") + } + }) +} + +// 旧 .so 插件必须报「用新 plugindev 重编」而非静默跳过。 +func TestTryDynamic_LegacyCABIGivesActionableError(t *testing.T) { + r := NewRegistry() + defer r.closeProcHost() + + dir := t.TempDir() + mustWrite(t, filepath.Join(dir, "plugin.so"), "old cabi binary") + + _, err := r.tryDynamic(dir, "legacy", nil) + if err == nil { + t.Fatal("旧 C ABI 产物应报错,不得静默跳过") + } + // 错误消息须指向解决办法,且明确业务代码无需改 + msg := err.Error() + for _, want := range []string{"plugindev", "plugin.bin", "业务代码"} { + if !strings.Contains(msg, want) { + t.Errorf("错误消息应含 %q,实际: %v", want, err) + } + } +} + +// entry 声明 plugin.bin 但二进制缺失时返回 nil,nil(交由后续探测)。 func TestTryLoadProc_MissingBinaryReturnsNil(t *testing.T) { dir := t.TempDir() plg, err := tryLoadProc(dir, "demo", nil) if plg != nil || err != nil { - t.Fatalf("无 plugin.bin 应返回 nil,nil(交由后续探测),实际 plg=%v err=%v", plg, err) + t.Fatalf("无 plugin.bin 应返回 nil,nil,实际 plg=%v err=%v", plg, err) } } @@ -123,9 +161,8 @@ func TestTryLoadProc_NonExecutableRejected(t *testing.T) { // pluginEntryHash 的候选顺序须与 detectEntryKind 一致(plugin.bin 优先), // 否则增量重载会用错文件算 hash,导致"换了 .bin 但内核以为没变"。 -func TestPluginEntryHash_PrefersBin(t *testing.T) { +func TestPluginEntryHash_UsesBin(t *testing.T) { dir := t.TempDir() - mustWrite(t, filepath.Join(dir, "plugin.so"), "so content") mustWrite(t, filepath.Join(dir, "plugin.bin"), "bin content") h1 := pluginEntryHash(dir) @@ -133,19 +170,22 @@ func TestPluginEntryHash_PrefersBin(t *testing.T) { t.Fatal("应算出 hash") } - // 改 .so 不应影响 hash(因为以 .bin 为准) - mustWrite(t, filepath.Join(dir, "plugin.so"), "so content CHANGED") - if h2 := pluginEntryHash(dir); h2 != h1 { - t.Error("plugin.bin 存在时 hash 不应受 plugin.so 变化影响") - } - - // 改 .bin 必须改变 hash mustWrite(t, filepath.Join(dir, "plugin.bin"), "bin content CHANGED") - if h3 := pluginEntryHash(dir); h3 == h1 { + if h2 := pluginEntryHash(dir); h2 == h1 { t.Error("plugin.bin 变化必须反映到 hash(否则增量重载失效)") } } +// C ABI 产物不再参与 hash 计算:内核已不认它,把它算进去会让 +// 「换了 .so」触发一次无意义的重载尝试。 +func TestPluginEntryHash_IgnoresLegacyCABI(t *testing.T) { + dir := t.TempDir() + mustWrite(t, filepath.Join(dir, "plugin.so"), "so content") + if h := pluginEntryHash(dir); h != "" { + t.Errorf("只有 .so 时应返回空串(C ABI 已退场),实际 %q", h) + } +} + func TestPluginEntryHash_EmptyForFactoryOnlyPlugin(t *testing.T) { if h := pluginEntryHash(t.TempDir()); h != "" { t.Errorf("无入口文件应返回空串(内置纯工厂插件),实际 %q", h) diff --git a/internal/plugin/registry.go b/internal/plugin/registry.go index 8755032..b67adfc 100644 --- a/internal/plugin/registry.go +++ b/internal/plugin/registry.go @@ -374,7 +374,7 @@ func (r *Registry) isDisabled(name string) bool { // plugin.bin 排在最前:与 detectEntryKind 保持一致的优先级,迁移期间同目录 // 两种产物共存时以子进程产物为准。 func pluginEntryHash(plgDir string) string { - for _, candidate := range []string{binEntry, soEntry, dllEntry, "plugin.dylib", luaEntry, skillEntry} { + for _, candidate := range []string{binEntry, luaEntry, skillEntry} { path := filepath.Join(plgDir, candidate) if data, err := os.ReadFile(path); err == nil && len(data) > 0 { sum := sha256.Sum256(data) @@ -905,8 +905,10 @@ func (r *Registry) PluginDir() string { } func (r *Registry) tryDynamic(plgDir, name string, config map[string]interface{}) (sdk.Plugin, error) { - // 按 manifest entry 分派到对应加载通道(外部插件多进程化:.so/.dll 与 .bin 双通道共存)。 - // 这使迁移可逐插件推进、随时回退——把 entry 改回 plugin.so 即回到旧通道。 + // 按 manifest entry 分派加载通道。 + // + // C ABI 通道(.so/.dll/.dylib)已整体删除:外部插件统一走子进程, + // 三套独立 ABI 实现收敛为单一 RPC 实现(§9.2)。 if detectEntryKind(plgDir) == entryProc { plg, err := r.loadProc(plgDir, name, config) if err != nil { @@ -916,29 +918,21 @@ func (r *Registry) tryDynamic(plgDir, name string, config map[string]interface{} log.Printf("[plugin] %s: 经 proc 通道加载(子进程)", name) return plg, nil } - // entry 声明了 plugin.bin 但文件不存在/不可用 → 不隐式回退到 cabi, - // 否则"已迁移插件静默跑回旧通道"极难排查。 return nil, fmt.Errorf("plugin %s: entry 声明 %s 但未找到可用二进制", name, binEntry) } - // 既有探测顺序(保持不变):.so → .dll → .lua - for _, try := range []struct { - name string - fn func(string, string, map[string]interface{}) (sdk.Plugin, error) - }{ - {"so", tryLoadSO}, - {"dll", tryLoadDLL}, - {"lua", tryLoadLua}, - } { - plg, err := try.fn(plgDir, name, config) - if err != nil { - return nil, err - } - if plg != nil { - return plg, nil - } + // 旧 .so/.dll 插件给明确错误,不静默跳过。 + // 静默跳过会让「插件目录在但没加载」看起来像配置问题, + // 而实际原因是需要用新 plugindev 重编。 + if hasLegacyCABIEntry(plgDir) { + return nil, fmt.Errorf( + "plugin %s: 检测到旧 C ABI 产物(plugin.so/.dll/.dylib)。"+ + "外部插件已改为子进程模式,请用新版 plugindev 重编产出 %s"+ + "(业务代码无需修改)", name, binEntry) } - return nil, nil + + // Lua 插件仍走解释器 + return tryLoadLua(plgDir, name, config) } func (r *Registry) readConfig(plgDir string) map[string]interface{} { diff --git a/internal/plugins/pluginmgr/plugin.go b/internal/plugins/pluginmgr/plugin.go index 7f683c3..9ecf0a0 100644 --- a/internal/plugins/pluginmgr/plugin.go +++ b/internal/plugins/pluginmgr/plugin.go @@ -23,16 +23,16 @@ import ( sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk" ) -// platformBinary 按当前 OS 选择正确的插件二进制文件名。 +// platformBinary 按当前 OS/ARCH 选择正确的插件二进制文件名。 // 返回 (zip内文件名, 安装后重命名). +// +// 子进程模式下各平台产物统一叫 plugin.bin(进程边界即 ABI 边界, +// 不存在 .so/.dylib/.dll 的区分),故 zip 内按平台加后缀区分, +// 解包时挑当前平台那一份重命名为 plugin.bin。 func platformBinary() (zipName, canonicalName string) { switch runtime.GOOS { - case "linux": - return "plugin.so", "plugin.so" - case "darwin": - return "plugin.dylib", "plugin.so" // dlopen 兼容 .so 名称 - case "windows": - return "plugin.dll", "plugin.dll" + case "linux", "darwin", "windows", "freebsd": + return fmt.Sprintf("plugin.bin.%s.%s", runtime.GOOS, runtime.GOARCH), "plugin.bin" default: return "", "" } @@ -40,18 +40,17 @@ func platformBinary() (zipName, canonicalName string) { // validBinaries 是 .hmap 中所有可识别的文件入口(平台二进制或脚本)。 var validBinaries = map[string]bool{ - "plugin.so": true, - "plugin.dylib": true, - "plugin.dll": true, - "main.lua": true, - "SKILL.md": true, + "plugin.bin": true, + "main.lua": true, + "SKILL.md": true, } -// platformBinaries 是平台特定的二进制,bundle 模式下仅当前平台的被解压。 -var platformBinaries = map[string]bool{ - "plugin.so": true, - "plugin.dylib": true, - "plugin.dll": true, +// isPlatformBinary 判断 zip 条目是否为平台特定二进制(bundle 模式下仅当前平台的被解压)。 +// +// 形式:plugin.bin..。不用固定表是因为平台组合会增长 +// (linux/arm64、darwin/arm64 等),按前缀判断无需维护清单。 +func isPlatformBinary(name string) bool { + return strings.HasPrefix(name, "plugin.bin.") } var downloadClient = &http.Client{ @@ -705,17 +704,19 @@ func validatePackage(data []byte) (*pluginPackage, error) { } if len(pkg.Platforms) > 0 { - // bundle mode: check each declared platform has a matching binary + // bundle mode:每个声明的平台都要有对应二进制。 + // 子进程模式下条目形式为 plugin.bin.., + // 故按前缀匹配而不枚举架构(同一 OS 可能有 amd64/arm64 两份)。 for _, plat := range pkg.Platforms { - bin, ok := map[string]string{ - "linux": "plugin.so", - "darwin": "plugin.dylib", - "windows": "plugin.dll", - }[plat] - if !ok { - return nil, fmt.Errorf("unsupported platform: %q", plat) + prefix := "plugin.bin." + plat + "." + found := false + for name := range zipEntries { + if strings.HasPrefix(name, prefix) { + found = true + break + } } - if zipEntries[bin] { + if found { hasBinary = true } } @@ -795,11 +796,11 @@ func extractPackage(data []byte, pluginDir string) error { } // bundle mode: skip other platforms' platform-specific binaries - if isBundle && platformBinaries[f.Name] && f.Name != zipBin { + if isBundle && isPlatformBinary(f.Name) && f.Name != zipBin { continue } - // rename platform binary to canonical name (e.g. plugin.dylib → plugin.so) + // 平台二进制重命名为规范名(plugin.bin.linux.amd64 → plugin.bin) dest := fpath if isBundle && f.Name == zipBin && canonicalName != zipBin { dest = filepath.Join(target, canonicalName) @@ -808,6 +809,15 @@ func extractPackage(data []byte, pluginDir string) error { if err := copyZipEntry(f, dest); err != nil { return err } + + // 子进程插件必须可执行。 + // zip 保留了原文件权限位,但经某些工具链/传输后可能丢失; + // 内核加载时会因缺执行位报错(带 chmod +x 提示),在此提前补上。 + if filepath.Base(dest) == "plugin.bin" { + if err := os.Chmod(dest, 0o755); err != nil { + return fmt.Errorf("chmod %s: %w", dest, err) + } + } } return nil diff --git a/internal/plugins/pluginmgr/upgrade_test.go b/internal/plugins/pluginmgr/upgrade_test.go index e5ce59b..063ac4f 100644 --- a/internal/plugins/pluginmgr/upgrade_test.go +++ b/internal/plugins/pluginmgr/upgrade_test.go @@ -89,12 +89,12 @@ func buildHmap(t *testing.T, name, version string) []byte { zw := zip.NewWriter(&buf) manifest := map[string]interface{}{ "name": name, "name_zh": name, "name_en": name, - "version": version, "entry": "plugin.so", + "version": version, "entry": "plugin.bin", } mData, _ := json.Marshal(manifest) f, _ := zw.Create("plugin.json") f.Write(mData) - bin, _ := zw.Create("plugin.so") + bin, _ := zw.Create("plugin.bin") bin.Write([]byte("binary-" + name + "-" + version)) zw.Close() return buf.Bytes() @@ -152,12 +152,12 @@ func TestInstallThenUpgradeKeepsConfig(t *testing.T) { t.Fatalf("StopAndUnload not called once with demo: %v", calls) } // 新二进制写入 - soData, err := os.ReadFile(filepath.Join(dir, "demo", "plugin.so")) + binData, err := os.ReadFile(filepath.Join(dir, "demo", "plugin.bin")) if err != nil { - t.Fatalf("read new so: %v", err) + t.Fatalf("read new bin: %v", err) } - if string(soData) != "binary-demo-2.0.0" { - t.Fatalf("so not overwritten: %q", string(soData)) + if string(binData) != "binary-demo-2.0.0" { + t.Fatalf("bin not overwritten: %q", string(binData)) } // 4. 降级 v2.0.0 → v1.5.0 @@ -187,7 +187,7 @@ func TestExtractFailureRollsBack(t *testing.T) { // 构造损坏包:zip 但缺 plugin.json(extractPackage 会失败) var buf bytes.Buffer zw := zip.NewWriter(&buf) - f, _ := zw.Create("plugin.so") + f, _ := zw.Create("plugin.bin") f.Write([]byte("corrupt")) zw.Close() @@ -200,7 +200,7 @@ func TestExtractFailureRollsBack(t *testing.T) { f2, _ := zw2.Create("../../evil") f2.Write([]byte("x")) mf, _ := zw2.Create("plugin.json") - mData, _ := json.Marshal(map[string]interface{}{"name": "rollback", "version": "9.9.9", "entry": "plugin.so"}) + mData, _ := json.Marshal(map[string]interface{}{"name": "rollback", "version": "9.9.9", "entry": "plugin.bin"}) mf.Write(mData) zw2.Close() bad = rb.Bytes() diff --git a/third_party/homeagent-sdk/tools/plugindev/templates.go b/third_party/homeagent-sdk/tools/plugindev/templates.go deleted file mode 100644 index ae30b9d..0000000 --- a/third_party/homeagent-sdk/tools/plugindev/templates.go +++ /dev/null @@ -1,1296 +0,0 @@ -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}} -` - -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.RegisterStopHandler(func() { fmt.Printf("[%s] stop handler running\n", p.name) }) - 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{}{}}, - NoMemory: false, // 工具输出对 LLM 注意力有信号价值时为 false,纯操作工具为 true - // Cleaner: func(output string) string { - // // 工具输出参与向量化/jieba/蒸馏前,在此过滤噪音 - // return output - // }, - }, 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 NewPluginFactory(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, scope) print("[lua-plugin] register_stage: " .. tostring(stage) .. " scope=" .. tostring(scope)) end -function sdk.register_api(name) print("[lua-plugin] register_api: " .. tostring(name)) end -function sdk.register_output_channel(name, caps, desc, def, handler) print("[lua-plugin] register_output_channel: " .. tostring(name)) end -function sdk.register_input_channel(name, def) print("[lua-plugin] register_input_channel: " .. 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 -function sdk.set_auto_restart(enabled) print("[lua-plugin] set_auto_restart: " .. tostring(enabled)) end -sdk.memory = {} -function sdk.memory.recall(query, depth) return {entities={}, relations={}} end -function sdk.memory.commit(triples) return nil end -function sdk.memory.introspect() return {} end -function sdk.memory.merge(source, target) return 0 end -function sdk.memory.purge(criteria, hard) return 0 end -sdk.doc = {} -function sdk.doc.query(text, top_k) return {} end -function sdk.doc.insert(doc) return nil end -function sdk.doc.remove(id) return nil end -function sdk.doc.stats() return {} end -sdk.knowledge = {} -function sdk.knowledge.search(query, limit) return {} end -function sdk.knowledge.add(tag, content) return nil end -function sdk.knowledge.list() return {} end -sdk.text_memory = {} -function sdk.text_memory.append(evt) return nil end -sdk.llm = {} -function sdk.llm.list_sources() return {} end -function sdk.llm.set_source(name) return nil end -function sdk.llm.current_source() return nil end -sdk.social = {} -function sdk.social.get_person(name) return {} end -function sdk.social.get_network(name, depth) return {} end -function sdk.social.get_trait(name, trait) return {value=nil, found=false} end -function sdk.social.get_relations(name) return {} end -function sdk.social.list_persons() return {} end -sdk.settings = {} -function sdk.settings.get_core(key) return nil end -function sdk.settings.set_core(key, value) return nil end -function sdk.settings.list_core(prefix) return {} end -function sdk.settings.get_plugin(plugin, key) return nil end -function sdk.settings.set_plugin(plugin, key, value) return nil end -function sdk.settings.list_plugin(plugin, prefix) return {} end -function sdk.settings.list(prefix) return {} end -function sdk.settings.register_def(def) return nil end -function sdk.settings.defs(prefix) return {} end -function sdk.settings.dump() return {} end -function sdk.settings.plugins() return {} 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 -*/ -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{} - sdk *sdk.PluginSDK -} - -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 := NewPluginFactory(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 }, - func(name string, caps int, desc string, def sdk.ChannelDef, handler sdk.ToolHandler) error { return nil }, - ) - mockSDK.SetInputChannelRegistrar(func(name string, def sdk.ChannelDef) error { return nil }) - bs.sdk = mockSDK - 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 bs.sdk != nil { - bs.sdk.RunStopHandlers() - } - 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 { 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 { errMsg, _ := json.Marshal(map[string]interface{}{"error": err.Error()}); return C.CString(string(errMsg)) } - 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 -// 此模板中的常量应与 core/internal/meta/meta.go 保持一致(ABI 版本、dispatch method IDs)。 -const tmplCABIHeader = ` -#ifndef HOMEAGENT_CABI_H -#define HOMEAGENT_CABI_H -// HOMEAGENT_ABI_VERSION 与 sdk/meta/meta.go CABINum 同步(major*100+minor,v0.9.x→900) -#define HOMEAGENT_ABI_VERSION 900 -#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**, 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_INJECT_INPUT_SYNC = 47, - 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, - CORE_REGISTER_INPUT_CH = 46, - CORE_INJECT_INPUT_SYNC = 47, - CORE_PLUGIN_RELOAD_ONE = 48, - CORE_PLUGIN_LIST_LOADED = 49, - CORE_PLUGIN_IS_DISABLED = 50, -}; - -#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 -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 - currentSDK *sdk.PluginSDK - coreAPI unsafe.Pointer - - handlerMu sync.RWMutex - coreAPIMu 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 { - coreAPIMu.RLock() - api := coreAPI - coreAPIMu.RUnlock() - 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), api, 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) { - coreAPIMu.RLock() - api := coreAPI - coreAPIMu.RUnlock() - 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), api, 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), api, 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, def sdk.ChannelDef, handler sdk.ToolHandler) error { - handlerMu.Lock() - outputHandlers[name] = handler - handlerMu.Unlock() - defJSON, _ := json.Marshal(def) - return callVoid(3, name, desc, string(defJSON), caps, 0) - }, - ) - base.SetIOInjector(dispatchIO{}) - base.SetMemoryAPI(dispatchMemory{}) - base.SetDocMemoryAPI(dispatchDocMemory{}) - base.SetKnowledgeAPI(dispatchKnowledge{}) - base.SetLLMAPI(dispatchLLM{}) - base.SetSocialAPI(dispatchSocial{}) - base.SetTextMemoryAPI(dispatchTextMemory{}) - base.SetPluginMgrAPI(dispatchPluginMgr{}) - base.SetInputChannelRegistrar( - func(name string, def sdk.ChannelDef) error { - defJSON, _ := json.Marshal(def) - return callVoid(46, name, string(defJSON), "", 0, 0) - }, - ) - 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) } -func (dispatchIO) InjectInputSync(s, c, t string) string { r, _ := callString(47, s, c, t, 0, 0); return r } -// SetToolBlocks 是 Go 原生(非 ABI)的多模态注入;跨 ABI 的外部插件无对应内核桥接, -// 故为空实现(满足接口即可)。需要多模态块时用插件内自持 SDK,不走 ABI。 -func (dispatchIO) SetToolBlocks([]sdk.ContentBlock) {} - -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) } - -// ---- dispatchPluginMgr (CORE_PLUGIN_RELOAD_ONE = 48) ---- - -type dispatchPluginMgr struct{} - -func (dispatchPluginMgr) ReloadOne(name string) error { - return callVoid(48, name, "", "", 0, 0) -} - -func (dispatchPluginMgr) ListLoadedPlugins() []string { - r, e := callString(49, "", "", "", 0, 0) - if e != nil || r == "" { - return nil - } - var list []string - if json.Unmarshal([]byte(r), &list) != nil { - return nil - } - return list -} - -func (dispatchPluginMgr) IsPluginDisabled(name string) bool { - r, e := callString(50, name, "", "", 0, 0) - return e == nil && r == "1" -} - -// ---- 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 -} -func (d *dispatchSettings) DataDir() string { - r, e := callString(51, "", "", "", 0, 0); if e != nil { return "" }; return r -} - -// ---- 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 := NewPluginFactory(C.GoString(name), nil) - if err != nil || plg == nil { - if err != nil { *errorOut = C.CString(err.Error()) } else { *errorOut = C.CString("NewPluginFactory 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 - coreAPIMu.Lock() - coreAPI = coreAPIptr - coreAPIMu.Unlock() - mu.Unlock() - _ = coreVersion - if plg == nil { *errorOut = C.CString("not initialized"); return 1 } - sdk := buildPluginSDK(plg.Name()) - mu.Lock(); currentSDK = sdk; mu.Unlock() - 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 - sdk := currentSDK - currentPlg = nil - currentSDK = nil - coreAPIMu.Lock() - coreAPI = nil - coreAPIMu.Unlock() - mu.Unlock() - if sdk != nil { - sdk.RunStopHandlers() - } - 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 -} - -// fillStageContext 将内核传来的 ctx JSON 填充到插件侧 StageContext。 -func fillStageContext(sc *sdk.StageContext, ctxJSON string) { - var m map[string]interface{} - if err := json.Unmarshal([]byte(ctxJSON), &m); err != nil { - return - } - 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) - } -} - -// stageContextWritable 提取插件可写且内核会同步回去的字段。 -func stageContextWritable(sc *sdk.StageContext) map[string]interface{} { - m := map[string]interface{}{ - "raw_message": sc.RawMessage, - "user_id": sc.UserID, - "group_id": sc.GroupID, - "phase": string(sc.Phase), - "llm_text": sc.LLMText, - "final_text": sc.FinalText, - "no_memory": sc.NoMemory, - } - if sc.Response != nil { - m["response"] = *sc.Response - } - if len(sc.ToolCalls) > 0 { - m["tool_calls"] = sc.ToolCalls - } - if len(sc.ToolResults) > 0 { - m["tool_results"] = sc.ToolResults - } - return m -} - -// changedFieldsOnly 返回插件 handler 真正变更的字段,供内核写回。 -// 修复 plan.md 11.3:旧实现无条件回传 stageContextWritable 的全部字段(含插件 -// 从内核收到的旧快照),两个插件并发时,只读插件会把自己收到的旧值覆盖回 -// 改写插件已清洗的结果(实验 13 复刻现网 sanitizer + weather 场景,丢失率 1.6~4.3%)。 -// 只回传差异字段后,只读插件零回传,改写插件的清洗结果不再被覆盖。 -// -// ❗ before 必须是 handler 运行前的**序列化快照**(snapshotWritable),不能直接存 Go 值: -// stageContextWritable 返回的 tool_calls/tool_results 与 sc 共享切片底层数组,handler -// 原地修改元素(如 sc.ToolResults[0].Result = clean)会让 before 同步变化,diff 将看不到变更。 -func changedFieldsOnly(before map[string]string, after map[string]interface{}) map[string]interface{} { - diff := map[string]interface{}{} - keys := map[string]bool{} - for k := range before { - keys[k] = true - } - for k := range after { - keys[k] = true - } - for k := range keys { - bRaw, bHas := before[k] - a, aHas := after[k] - switch { - case aHas && !bHas: - diff[k] = a - case aHas && bHas: - ab, _ := json.Marshal(a) - if bRaw != string(ab) { - diff[k] = a - } - case bHas && !aHas: - // 插件把切片类字段清空了(writable 对 len==0 不输出),显式回传空值 - switch k { - case "tool_calls": - diff[k] = []sdk.ToolCall{} - case "tool_results": - diff[k] = []sdk.ToolResult{} - case "response": - // response 从非 nil 变 nil:内核侧 applyStageResult 无法表达「清空」, - // 且短路语义不应被插件撑销,故不回传。 - } - } - } - return diff -} - -// snapshotWritable 把 writable 字段逐个序列化成 JSON 字符串,作为 handler 前的不可变快照。 -// 必须序列化:否则切片字段与 sc 共享底层数组,handler 原地改元素时快照跟着变,diff 失效。 -func snapshotWritable(sc *sdk.StageContext) map[string]string { - snap := map[string]string{} - for k, v := range stageContextWritable(sc) { - b, err := json.Marshal(v) - if err != nil { - continue - } - snap[k] = string(b) - } - return snap -} - -//export go_invoke_stage -func go_invoke_stage(stage *C.char, ctxJSON *C.char, resultOut **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 { - fillStageContext(sc, C.GoString(ctxJSON)) - } - // plan.md 11.3:记录 handler 前的**序列化**快照,回传时只带真正变更的字段, - // 避免只读插件把自己收到的旧快照覆盖其他插件的改写(lost update)。 - before := snapshotWritable(sc) - if err := h(sc); err != nil { *errorOut = C.CString(err.Error()); return 1 } - // ABI v2: 回传插件修改后的上下文(若调用方要求)——只回传差异字段 - if resultOut != nil { - diff := changedFieldsOnly(before, stageContextWritable(sc)) - if len(diff) == 0 { - return 0 // 无变更(如只读插件)→ 不回传,内核不写回 - } - if b, err := json.Marshal(diff); err == nil { - *resultOut = C.CString(string(b)) - } - } - 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 -#include - -// HOMEAGENT_ABI_VERSION 与 sdk/meta/meta.go CABINum 同步(major*100+minor,v0.9.x→900) -#define HOMEAGENT_ABI_VERSION 900 - -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**, 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**, 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** r, char** e) { return go_invoke_stage(s, c, r, 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; -} -` - -// ============================================================ -// Remote Device Adapter Templates -// ============================================================ - -const tmplRemoteDeviceMain = `#include -#include -#include - -#include "ha_remotedevice.h" - -/* ============================================================ - * {{.Plg.Name}} — Remote Device Adapter - * - * 声明式远程设备接入示例。 - * 用户只需实现: - * 1. ha_transport_t 的 4 个函数 - * 2. 声明 handlers 表(设备支持哪些命令 + 对应的处理函数) - * 其余协议细节(WS 握手、hello/bind、心跳、重连、命令分发、结果回执)由 SDK 自动处理。 - * ============================================================ */ - -/* ====================== 传输层实现 ====================== - * - * 请为你的平台实现以下 4 个函数: - * connect(ctx, host, port) — 建立 TCP 连接 - * send(ctx, data, len) — 发送数据 - * recv(ctx, buf, len) — 接收数据(阻塞,返回实际接收字节数) - * close(ctx) — 关闭连接 - * - * 示例:POSIX socket 实现 - */ - -#if defined(_WIN32) || defined(_WIN64) -/* Windows 平台需包含 winsock2.h */ -#error "Please implement transport for your platform (see example below)" -#else -/* POSIX (Linux, macOS, ESP-IDF, Zephyr, etc.) */ -#include -#include -#include -#include -#include - -struct transport_ctx { - int sock; -}; - -static int transport_connect(void *ctx, const char *host, uint16_t port) { - struct transport_ctx *tc = (struct transport_ctx *)ctx; - struct hostent *he = gethostbyname(host); - if (!he) return -1; - tc->sock = socket(AF_INET, SOCK_STREAM, 0); - if (tc->sock < 0) return -1; - struct sockaddr_in addr; - memset(&addr, 0, sizeof(addr)); - addr.sin_family = AF_INET; - addr.sin_port = htons(port); - memcpy(&addr.sin_addr, he->h_addr_list[0], he->h_length); - if (connect(tc->sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) { - close(tc->sock); - tc->sock = -1; - return -1; - } - return 0; -} - -static int transport_send(void *ctx, const uint8_t *data, int len) { - struct transport_ctx *tc = (struct transport_ctx *)ctx; - int sent = 0; - while (sent < len) { - int n = (int)send(tc->sock, data + sent, len - sent, 0); - if (n <= 0) return -1; - sent += n; - } - return sent; -} - -static int transport_recv(void *ctx, uint8_t *buf, int len) { - struct transport_ctx *tc = (struct transport_ctx *)ctx; - int n = (int)recv(tc->sock, buf, len, 0); - return n; -} - -static void transport_close(void *ctx) { - struct transport_ctx *tc = (struct transport_ctx *)ctx; - if (tc->sock >= 0) { - close(tc->sock); - tc->sock = -1; - } -} -#endif - -/* ====================== 声明式命令处理 ====================== - * - * 每个命令对应一个处理函数,通过填写 ha_cmd_result_t 返回数据。 - * SDK 自动回执结果,无需手动调用 send_result。 - * - * 返回方式: - * 1. 文本输出:填写 result->output - * 2. 二进制数据:设置 result->has_binary=1 并填写 binary_data/len/mime - * 3. 错误:设置 result->status=1 并填写 result->error - * 4. 返回 HA_OK 表示处理成功,其他值表示处理失败 - */ - -/* ESP32-CAM 摄像头处理 */ -static ha_status_t handle_camerasue(const char *req_id, const char *args, - ha_cmd_result_t *result, void *userdata) { - (void)req_id; (void)userdata; - int duration = 0; - if (args && args[0]) duration = atoi(args); - printf("[camera] %s (duration=%ds)\n", duration ? "record" : "snapshot", duration); - - /* 返回文本结果(base64 图片) */ - result->status = 0; - result->output = "data:image/jpeg;base64,/9j/4AAQ..."; - return HA_OK; -} - -/* 屏幕截图处理 */ -static ha_status_t handle_screensee(const char *req_id, const char *args, - ha_cmd_result_t *result, void *userdata) { - (void)req_id; (void)args; (void)userdata; - printf("[screen] screenshot\n"); - result->status = 0; - result->output = "data:image/png;base64,iVBORw0KGgo..."; - return HA_OK; -} - -/* 语音播报处理 */ -static ha_status_t handle_speakeruse(const char *req_id, const char *args, - ha_cmd_result_t *result, void *userdata) { - (void)req_id; (void)userdata; - printf("[speaker] TTS: %s\n", args ? args : ""); - result->status = 0; - result->output = "speakeruse done"; - return HA_OK; -} - -/* 远程操控处理(computeruse) */ -static ha_status_t handle_computeruse(const char *req_id, const char *args, - ha_cmd_result_t *result, void *userdata) { - (void)req_id; (void)userdata; - const char *action = NULL; - const char *json_str = NULL; - ha_cmd_parse_json(args, &action, &json_str); - printf("[computeruse] action=%s\n", action ? action : "unknown"); - result->status = 0; - result->output = "computeruse done"; - return HA_OK; -} - -/* 剪贴板读取 */ -static ha_status_t handle_clipboardsee(const char *req_id, const char *args, - ha_cmd_result_t *result, void *userdata) { - (void)req_id; (void)args; (void)userdata; - result->status = 0; - result->output = "clipboard content"; - return HA_OK; -} - -/* 剪贴板写入 */ -static ha_status_t handle_clipboardsue(const char *req_id, const char *args, - ha_cmd_result_t *result, void *userdata) { - (void)req_id; (void)userdata; - printf("[clipboard] write: %s\n", args ? args : ""); - result->status = 0; - result->output = "clipboard written"; - return HA_OK; -} - -/* 屏幕显示 */ -static ha_status_t handle_screensue(const char *req_id, const char *args, - ha_cmd_result_t *result, void *userdata) { - (void)req_id; (void)userdata; - printf("[screensue] show: %s\n", args ? args : ""); - result->status = 0; - result->output = "screensue shown"; - return HA_OK; -} - -/* Shell 命令处理 */ -static ha_status_t handle_shell(const char *req_id, const char *args, - ha_cmd_result_t *result, void *userdata) { - (void)req_id; (void)userdata; - printf("[shell] cmd: %s\n", args ? args : ""); - result->status = 0; - result->output = "shell output"; - return HA_OK; -} - -/* 设备信息查询 */ -static ha_status_t handle_deviceinfo(const char *req_id, const char *args, - ha_cmd_result_t *result, void *userdata) { - (void)req_id; (void)args; (void)userdata; - result->status = 0; - result->output = "{\"platform\":\"linux\",\"arch\":\"x86_64\"}"; - return HA_OK; -} - -/* ====================== 连接状态回调 ====================== */ - -static void on_state(int connected, void *userdata) { - (void)userdata; - printf("[devicelink] state: %s\n", connected ? "connected" : "disconnected"); -} - -/* ====================== 主函数 ====================== */ - -int main(int argc, char *argv[]) { - /* 传输层上下文 */ - struct transport_ctx tctx; - tctx.sock = -1; - - ha_transport_t transport = { - .connect = transport_connect, - .send = transport_send, - .recv = transport_recv, - .close = transport_close, - .ctx = &tctx, - }; - - /* ===== 声明式设备配置 ===== */ - - /* 声明设备能力 */ - const char *caps[] = { - "status", "cmdrun", "deviceinfo", - "camerasue", "screensee", "speakeruse", - "computeruse", "clipboardsee", "clipboardsue", - "screensue", - NULL - }; - - /* 声明命令处理表:设备支持哪些命令,以及对应的处理函数 */ - ha_cmd_handler_def_t handlers[] = { - {.command = "shell", .handler = handle_shell}, - {.command = "camerasue", .handler = handle_camerasue}, - {.command = "screensee", .handler = handle_screensee}, - {.command = "speakeruse", .handler = handle_speakeruse}, - {.command = "computeruse", .handler = handle_computeruse}, - {.command = "clipboardsee", .handler = handle_clipboardsee}, - {.command = "clipboardsue", .handler = handle_clipboardsue}, - {.command = "screensue", .handler = handle_screensue}, - {.command = "deviceinfo", .handler = handle_deviceinfo}, - {.command = NULL}, /* 标记结束 */ - }; - - ha_config_t config = { - .transport = transport, - .server = "127.0.0.1:9890", - .token = "your-token-here", - .device = { - .device_id = "{{.Plg.Name}}", - .name = "{{.Plg.NameEn}}", - .kind = "computer", - .caps = caps, - .info_json = "{\"platform\":\"linux\",\"arch\":\"x86_64\"}", - }, - .handlers = handlers, /* 声明式命令处理表 */ - .on_state = on_state, - .ping_interval = 30, - }; - - ha_client_t *client = ha_client_new(&config); - if (!client) { - fprintf(stderr, "Failed to create client\n"); - return 1; - } - - printf("Starting remote device adapter: {{.Plg.Name}}\n"); - printf(" Server: %s\n", config.server); - printf(" Device ID: %s\n", config.device.device_id); - printf(" Kind: %s\n", config.device.kind); - printf(" Caps: "); - for (const char **p = caps; *p; p++) printf("%s ", *p); - printf("\n"); - - ha_status_t st = ha_client_start(client); - if (st != HA_OK) { - fprintf(stderr, "Failed to connect: %d\n", st); - ha_client_destroy(client); - return 1; - } - - printf("Connected! Entering main loop...\n"); - - /* 主循环 */ - while (1) { - ha_status_t st = ha_client_process(client); - if (st == HA_ERR_DISCONNECTED) { - printf("Disconnected, exiting.\n"); - break; - } -#if defined(_WIN32) || defined(_WIN64) - Sleep(10); -#else - usleep(10000); -#endif - } - - ha_client_stop(client); - ha_client_destroy(client); - return 0; -} -` - -const tmplRemoteDeviceCMake = `cmake_minimum_required(VERSION 3.10) -project({{.Plg.Name}} VERSION 0.1.0 LANGUAGES C) - -# ============================================================ -# {{.Plg.Name}} — Remote Device Adapter -# ============================================================ - -# 设置 SDK 路径(默认使用内置 SDK,也可通过 -DSDK_PATH=... 指定) -set(SDK_PATH "${CMAKE_CURRENT_SOURCE_DIR}/ha_remotedevice" - CACHE PATH "Path to ha_remotedevice SDK") - -# 添加 SDK 子目录 -if(EXISTS "${SDK_PATH}/CMakeLists.txt") - add_subdirectory(${SDK_PATH} ha_remotedevice) -else() - message(FATAL_ERROR "ha_remotedevice SDK not found at ${SDK_PATH}") -endif() - -# 创建设备适配器可执行文件 -add_executable(${PROJECT_NAME} - main.c -) - -# 链接 SDK -target_link_libraries(${PROJECT_NAME} PRIVATE ha_remotedevice) - -# 包含 SDK 头文件 -target_include_directories(${PROJECT_NAME} PRIVATE - ${HA_REMOTEDEVICE_INCLUDE_DIR} -) - -# 编译选项 -if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") - target_compile_options(${PROJECT_NAME} PRIVATE - -Wall -Wextra -Wpedantic - -Wno-unused-parameter - ) -endif() - -# 安装 -install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin) -` - -const tmplReadme = `# {{.Plg.Name}} - -{{.Plg.Description}} - -## Build - -` + "```bash" + ` -plugindev build -` + "```" + ` - -## Install - -Upload the .hmap file through the Plugin Manager API. -`