mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
plugin: 删除 C ABI 通道(Part 6.2 完成,-3198 行)
外部插件统一走子进程 + stdio RPC,三套独立 ABI 实现收敛为单一 RPC 实现。 用户决策:彻底舍弃 .so 能力,不保留双通道回退。 ## 删除清单 internal/plugin/cabi/ 1156 行(loader.go/loader.c/types.go/output_test.go) internal/plugin/dynamic_dll_windows.go 272 行(§9.2 记录的能力退化实现) internal/plugin/dynamic_loader_unix.go 79 行(唯一 cabi 引用点) internal/plugin/dynamic_dll_test.go 32 行 internal/plugin/dynamic_dll_stub.go 11 行 internal/plugin/dynamic_loader_windows.go 11 行 internal/plugin/bridge_e2e_test.go (测的是 cabi 路径) third_party/.../plugindev/templates.go 1296 行(取消跟踪,SDK 仓才是权威副本) dynamic.go:entryCABI 通道删除,soEntry/dllEntry 常量删除。 registry.go:tryDynamic 探测顺序从 .so → .dll → .lua 变成 proc → lua。 ## 旧 .so 给明确错误,不静默跳过 静默跳过会让「插件目录在但没加载」看起来像配置问题,而实际原因是需要 用新版 plugindev 重编。故保留 legacyCABIEntries 表专门用于识别残留: plugin legacy: 检测到旧 C ABI 产物(plugin.so/.dll/.dylib)。 外部插件已改为子进程模式,请用新版 plugindev 重编产出 plugin.bin (业务代码无需修改) 错误消息里「业务代码无需修改」这句是有测试守着的——迁移的核心承诺就是它。 ## pluginmgr 安装逻辑跟进 bundle 命名 子进程模式下各平台产物统一叫 plugin.bin(进程边界即 ABI 边界),故 zip 内 按平台加后缀 plugin.bin.<goos>.<goarch>,解包时挑当前平台那一份重命名。 platformBinary 改为按 runtime.GOOS+GOARCH 生成条目名;platformBinaries 固定表 换成 isPlatformBinary 前缀判断(平台组合会增长:linux/arm64、darwin/arm64…, 按前缀判断无需维护清单)。 新增 chmod 0755:zip 保留了原权限位,但经某些工具链/传输后可能丢失, 内核加载时会因缺执行位报错。提前补上比事后让用户 chmod 更好。 ## 测试 entry_dispatch_test.go 重写(12 项): - classifyEntry 对 .so/.dll/.dylib 现在返回 unknown - LegacyManifestFallsBackToProbe:存量插件 manifest 仍写 "plugin.so" (17 个插件没人去改),须靠目录探测找到 plugin.bin —— 这是 「外部插件零改动」的直接后果 - LegacyCABIGivesActionableError:错误消息须含 plugindev / plugin.bin / 业务代码 - PluginEntryHash_IgnoresLegacyCABI:.so 不参与 hash(内核已不认它) upgrade_test.go 的 .hmap 构造改用 plugin.bin。 验证:go build ./... 通过;go test ./... 全仓无失败; go test -race ./internal/plugin/... 全绿;三平台构建通过。 Ref: docs/zh/架构迁移评估.md §3.1/§9.2、docs/zh/plugin-migration-plan.md Part 6
This commit is contained in:
@ -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
|
|
||||||
}
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
//go:build linux || darwin
|
|
||||||
|
|
||||||
// HomeAgent C ABI loader — C implementation (compiled alongside Go code via cgo)
|
|
||||||
|
|
||||||
#include <dlfcn.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
// 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); }
|
|
||||||
@ -1,998 +0,0 @@
|
|||||||
//go:build linux || darwin
|
|
||||||
|
|
||||||
package cabi
|
|
||||||
|
|
||||||
/*
|
|
||||||
#cgo LDFLAGS: -ldl
|
|
||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
)
|
|
||||||
@ -7,31 +7,34 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
soEntry = "plugin.so"
|
binEntry = "plugin.bin" // 子进程插件(纯 Go 二进制,stdio JSON-RPC + 共享内存)
|
||||||
dllEntry = "plugin.dll"
|
|
||||||
binEntry = "plugin.bin" // 子进程插件(纯 Go 二进制,stdio JSON-RPC)
|
|
||||||
luaEntry = "main.lua"
|
luaEntry = "main.lua"
|
||||||
skillEntry = "SKILL.md"
|
skillEntry = "SKILL.md"
|
||||||
metaEntry = "plugin.json"
|
metaEntry = "plugin.json"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// legacyCABIEntries 是已退场的 C ABI 产物名。
|
||||||
|
//
|
||||||
|
// 保留这张表只为**给出明确错误**:插件目录里躺着 plugin.so 而内核不再认它时,
|
||||||
|
// 静默跳过会让「目录在但插件没加载」看起来像配置问题,而实际原因是需要用
|
||||||
|
// 新版 plugindev 重编。
|
||||||
|
var legacyCABIEntries = []string{"plugin.so", "plugin.dll", "plugin.dylib"}
|
||||||
|
|
||||||
// entryKind 描述插件入口归属的加载通道。
|
// entryKind 描述插件入口归属的加载通道。
|
||||||
// 外部插件多进程化期间 .so/.dll(cabi)与 .bin(proc)**双通道共存**,
|
//
|
||||||
// 按 plugin.json 的 entry 字段分派,使迁移可逐插件推进、随时回退。
|
// C ABI 通道(.so/.dll/.dylib)已整体退场:外部插件统一走子进程 + stdio RPC,
|
||||||
|
// 三套独立 ABI 实现收敛为单一 RPC 实现(§9.2)。
|
||||||
type entryKind int
|
type entryKind int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
entryUnknown entryKind = iota
|
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
|
entryLua // main.lua
|
||||||
entrySkill // SKILL.md
|
entrySkill // SKILL.md
|
||||||
)
|
)
|
||||||
|
|
||||||
func (k entryKind) String() string {
|
func (k entryKind) String() string {
|
||||||
switch k {
|
switch k {
|
||||||
case entryCABI:
|
|
||||||
return "cabi"
|
|
||||||
case entryProc:
|
case entryProc:
|
||||||
return "proc"
|
return "proc"
|
||||||
case entryLua:
|
case entryLua:
|
||||||
@ -43,11 +46,12 @@ func (k entryKind) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// classifyEntry 把 manifest 的 entry 字段映射到加载通道。
|
// classifyEntry 把 manifest 的 entry 字段映射到加载通道。
|
||||||
// entry 为空时返回 entryUnknown,由调用方回退到目录探测(兼容无 manifest 的旧插件)。
|
//
|
||||||
|
// entry 为空或声明已退场的 C ABI 产物时返回 entryUnknown,
|
||||||
|
// 由调用方回退到目录探测(兼容无 manifest 的旧插件),
|
||||||
|
// 并在探测到 C ABI 残留时给出明确的重编提示。
|
||||||
func classifyEntry(entry string) entryKind {
|
func classifyEntry(entry string) entryKind {
|
||||||
switch entry {
|
switch entry {
|
||||||
case soEntry, dllEntry, "plugin.dylib":
|
|
||||||
return entryCABI
|
|
||||||
case binEntry:
|
case binEntry:
|
||||||
return entryProc
|
return entryProc
|
||||||
case luaEntry:
|
case luaEntry:
|
||||||
@ -59,8 +63,9 @@ func classifyEntry(entry string) entryKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// detectEntryKind 先读 manifest 的 entry,读不到则按目录内存在的入口文件推断。
|
// detectEntryKind 先读 manifest 的 entry,读不到则按目录内存在的入口文件推断。
|
||||||
// 推断顺序:.bin 优先于 .so——迁移期间同一插件目录可能两个产物共存(升级未清理),
|
//
|
||||||
// 此时应走新通道;manifest 显式声明优先级最高。
|
// 注意:存量插件的 plugin.json 可能仍写着 "plugin.so"(工具链已不再据此分派,
|
||||||
|
// 但历史产物里有),此时 classifyEntry 返回 unknown,靠目录探测找到 plugin.bin。
|
||||||
func detectEntryKind(plgDir string) entryKind {
|
func detectEntryKind(plgDir string) entryKind {
|
||||||
if mft := readManifest(plgDir); mft != nil {
|
if mft := readManifest(plgDir); mft != nil {
|
||||||
if k := classifyEntry(mft.Entry); k != entryUnknown {
|
if k := classifyEntry(mft.Entry); k != entryUnknown {
|
||||||
@ -72,9 +77,6 @@ func detectEntryKind(plgDir string) entryKind {
|
|||||||
kind entryKind
|
kind entryKind
|
||||||
}{
|
}{
|
||||||
{binEntry, entryProc},
|
{binEntry, entryProc},
|
||||||
{soEntry, entryCABI},
|
|
||||||
{"plugin.dylib", entryCABI},
|
|
||||||
{dllEntry, entryCABI},
|
|
||||||
{luaEntry, entryLua},
|
{luaEntry, entryLua},
|
||||||
{skillEntry, entrySkill},
|
{skillEntry, entrySkill},
|
||||||
} {
|
} {
|
||||||
@ -85,6 +87,18 @@ func detectEntryKind(plgDir string) entryKind {
|
|||||||
return entryUnknown
|
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 {
|
func readManifest(dir string) *PluginManifest {
|
||||||
data, err := os.ReadFile(filepath.Join(dir, metaEntry))
|
data, err := os.ReadFile(filepath.Join(dir, metaEntry))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -96,5 +110,3 @@ func readManifest(dir string) *PluginManifest {
|
|||||||
}
|
}
|
||||||
return &m
|
return &m
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ = json.Marshal
|
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -3,26 +3,30 @@ package plugin
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// entry 分派骨架(docs/zh/plugin-migration-plan.md Part 1):
|
// entry 分派(docs/zh/plugin-migration-plan.md Part 1/6)。
|
||||||
// 外部插件多进程化期间 .so/.dll(cabi)与 .bin(proc)双通道共存,
|
//
|
||||||
// 按 plugin.json 的 entry 字段分派,使迁移可逐插件推进、随时回退。
|
// C ABI 通道(.so/.dll/.dylib)已整体退场:外部插件统一走子进程 + stdio RPC。
|
||||||
|
// 这些测试守住的是「旧产物给明确错误」而非「静默跳过」——后者会让
|
||||||
|
// 「插件目录在但没加载」看起来像配置问题。
|
||||||
|
|
||||||
func TestClassifyEntry(t *testing.T) {
|
func TestClassifyEntry(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
entry string
|
entry string
|
||||||
want entryKind
|
want entryKind
|
||||||
}{
|
}{
|
||||||
{"plugin.so", entryCABI},
|
|
||||||
{"plugin.dll", entryCABI},
|
|
||||||
{"plugin.dylib", entryCABI},
|
|
||||||
{"plugin.bin", entryProc},
|
{"plugin.bin", entryProc},
|
||||||
{"main.lua", entryLua},
|
{"main.lua", entryLua},
|
||||||
{"SKILL.md", entrySkill},
|
{"SKILL.md", entrySkill},
|
||||||
{"", entryUnknown},
|
{"", entryUnknown},
|
||||||
{"plugin.wasm", entryUnknown},
|
{"plugin.wasm", entryUnknown},
|
||||||
|
// 已退场的 C ABI 产物不再是有效通道
|
||||||
|
{"plugin.so", entryUnknown},
|
||||||
|
{"plugin.dll", entryUnknown},
|
||||||
|
{"plugin.dylib", entryUnknown},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
if got := classifyEntry(c.entry); got != c.want {
|
if got := classifyEntry(c.entry); got != c.want {
|
||||||
@ -34,45 +38,34 @@ func TestClassifyEntry(t *testing.T) {
|
|||||||
// manifest 显式声明的 entry 优先级最高。
|
// manifest 显式声明的 entry 优先级最高。
|
||||||
func TestDetectEntryKind_ManifestWins(t *testing.T) {
|
func TestDetectEntryKind_ManifestWins(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
// 目录里放 .so,但 manifest 声明 .bin → 应走 proc
|
mustWrite(t, filepath.Join(dir, "main.lua"), "fake lua")
|
||||||
mustWrite(t, filepath.Join(dir, "plugin.so"), "fake so")
|
|
||||||
mustWrite(t, filepath.Join(dir, "plugin.bin"), "fake bin")
|
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 {
|
if got := detectEntryKind(dir); got != entryLua {
|
||||||
t.Fatalf("manifest 声明 plugin.bin 应走 proc,实际 %v", got)
|
t.Fatalf("manifest 声明 main.lua 应走 lua,实际 %v", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// manifest 声明 .so 时即便存在 .bin 也走 cabi —— 这是回退路径的保证。
|
// 存量插件的 plugin.json 仍写着 "plugin.so"(历史产物),
|
||||||
func TestDetectEntryKind_ManifestCanForceRollback(t *testing.T) {
|
// 此时 classifyEntry 返回 unknown,须靠目录探测找到 plugin.bin。
|
||||||
|
//
|
||||||
|
// 这是「外部插件零改动」的直接后果:17 个插件的 manifest 没人去改。
|
||||||
|
func TestDetectEntryKind_LegacyManifestFallsBackToProbe(t *testing.T) {
|
||||||
dir := t.TempDir()
|
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, "plugin.bin"), "fake bin")
|
||||||
mustWrite(t, filepath.Join(dir, metaEntry), `{"name":"x","entry":"plugin.so"}`)
|
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 {
|
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) {
|
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()
|
dir := t.TempDir()
|
||||||
mustWrite(t, filepath.Join(dir, "plugin.so"), "x")
|
mustWrite(t, filepath.Join(dir, "plugin.bin"), "x")
|
||||||
if got := detectEntryKind(dir); got != entryCABI {
|
if got := detectEntryKind(dir); got != entryProc {
|
||||||
t.Fatalf("got %v", got)
|
t.Fatalf("got %v", got)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -95,15 +88,60 @@ func TestDetectEntryKind_ProbeFallbacks(t *testing.T) {
|
|||||||
t.Fatalf("空目录应为 unknown,实际 %v", got)
|
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 但二进制缺失时必须报明确错误,
|
// C ABI 残留必须能被识别,供 tryDynamic 给出「需要重编」的明确错误。
|
||||||
// 不得静默回退到 cabi —— 否则"已迁移插件跑回旧通道"极难排查。
|
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) {
|
func TestTryLoadProc_MissingBinaryReturnsNil(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
plg, err := tryLoadProc(dir, "demo", nil)
|
plg, err := tryLoadProc(dir, "demo", nil)
|
||||||
if plg != nil || err != 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 优先),
|
// pluginEntryHash 的候选顺序须与 detectEntryKind 一致(plugin.bin 优先),
|
||||||
// 否则增量重载会用错文件算 hash,导致"换了 .bin 但内核以为没变"。
|
// 否则增量重载会用错文件算 hash,导致"换了 .bin 但内核以为没变"。
|
||||||
func TestPluginEntryHash_PrefersBin(t *testing.T) {
|
func TestPluginEntryHash_UsesBin(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
mustWrite(t, filepath.Join(dir, "plugin.so"), "so content")
|
|
||||||
mustWrite(t, filepath.Join(dir, "plugin.bin"), "bin content")
|
mustWrite(t, filepath.Join(dir, "plugin.bin"), "bin content")
|
||||||
|
|
||||||
h1 := pluginEntryHash(dir)
|
h1 := pluginEntryHash(dir)
|
||||||
@ -133,19 +170,22 @@ func TestPluginEntryHash_PrefersBin(t *testing.T) {
|
|||||||
t.Fatal("应算出 hash")
|
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")
|
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(否则增量重载失效)")
|
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) {
|
func TestPluginEntryHash_EmptyForFactoryOnlyPlugin(t *testing.T) {
|
||||||
if h := pluginEntryHash(t.TempDir()); h != "" {
|
if h := pluginEntryHash(t.TempDir()); h != "" {
|
||||||
t.Errorf("无入口文件应返回空串(内置纯工厂插件),实际 %q", h)
|
t.Errorf("无入口文件应返回空串(内置纯工厂插件),实际 %q", h)
|
||||||
|
|||||||
@ -374,7 +374,7 @@ func (r *Registry) isDisabled(name string) bool {
|
|||||||
// plugin.bin 排在最前:与 detectEntryKind 保持一致的优先级,迁移期间同目录
|
// plugin.bin 排在最前:与 detectEntryKind 保持一致的优先级,迁移期间同目录
|
||||||
// 两种产物共存时以子进程产物为准。
|
// 两种产物共存时以子进程产物为准。
|
||||||
func pluginEntryHash(plgDir string) string {
|
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)
|
path := filepath.Join(plgDir, candidate)
|
||||||
if data, err := os.ReadFile(path); err == nil && len(data) > 0 {
|
if data, err := os.ReadFile(path); err == nil && len(data) > 0 {
|
||||||
sum := sha256.Sum256(data)
|
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) {
|
func (r *Registry) tryDynamic(plgDir, name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||||
// 按 manifest entry 分派到对应加载通道(外部插件多进程化:.so/.dll 与 .bin 双通道共存)。
|
// 按 manifest entry 分派加载通道。
|
||||||
// 这使迁移可逐插件推进、随时回退——把 entry 改回 plugin.so 即回到旧通道。
|
//
|
||||||
|
// C ABI 通道(.so/.dll/.dylib)已整体删除:外部插件统一走子进程,
|
||||||
|
// 三套独立 ABI 实现收敛为单一 RPC 实现(§9.2)。
|
||||||
if detectEntryKind(plgDir) == entryProc {
|
if detectEntryKind(plgDir) == entryProc {
|
||||||
plg, err := r.loadProc(plgDir, name, config)
|
plg, err := r.loadProc(plgDir, name, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -916,29 +918,21 @@ func (r *Registry) tryDynamic(plgDir, name string, config map[string]interface{}
|
|||||||
log.Printf("[plugin] %s: 经 proc 通道加载(子进程)", name)
|
log.Printf("[plugin] %s: 经 proc 通道加载(子进程)", name)
|
||||||
return plg, nil
|
return plg, nil
|
||||||
}
|
}
|
||||||
// entry 声明了 plugin.bin 但文件不存在/不可用 → 不隐式回退到 cabi,
|
|
||||||
// 否则"已迁移插件静默跑回旧通道"极难排查。
|
|
||||||
return nil, fmt.Errorf("plugin %s: entry 声明 %s 但未找到可用二进制", name, binEntry)
|
return nil, fmt.Errorf("plugin %s: entry 声明 %s 但未找到可用二进制", name, binEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 既有探测顺序(保持不变):.so → .dll → .lua
|
// 旧 .so/.dll 插件给明确错误,不静默跳过。
|
||||||
for _, try := range []struct {
|
// 静默跳过会让「插件目录在但没加载」看起来像配置问题,
|
||||||
name string
|
// 而实际原因是需要用新 plugindev 重编。
|
||||||
fn func(string, string, map[string]interface{}) (sdk.Plugin, error)
|
if hasLegacyCABIEntry(plgDir) {
|
||||||
}{
|
return nil, fmt.Errorf(
|
||||||
{"so", tryLoadSO},
|
"plugin %s: 检测到旧 C ABI 产物(plugin.so/.dll/.dylib)。"+
|
||||||
{"dll", tryLoadDLL},
|
"外部插件已改为子进程模式,请用新版 plugindev 重编产出 %s"+
|
||||||
{"lua", tryLoadLua},
|
"(业务代码无需修改)", name, binEntry)
|
||||||
} {
|
|
||||||
plg, err := try.fn(plgDir, name, config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if plg != nil {
|
|
||||||
return plg, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil, nil
|
|
||||||
|
// Lua 插件仍走解释器
|
||||||
|
return tryLoadLua(plgDir, name, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Registry) readConfig(plgDir string) map[string]interface{} {
|
func (r *Registry) readConfig(plgDir string) map[string]interface{} {
|
||||||
|
|||||||
@ -23,16 +23,16 @@ import (
|
|||||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
// platformBinary 按当前 OS 选择正确的插件二进制文件名。
|
// platformBinary 按当前 OS/ARCH 选择正确的插件二进制文件名。
|
||||||
// 返回 (zip内文件名, 安装后重命名).
|
// 返回 (zip内文件名, 安装后重命名).
|
||||||
|
//
|
||||||
|
// 子进程模式下各平台产物统一叫 plugin.bin(进程边界即 ABI 边界,
|
||||||
|
// 不存在 .so/.dylib/.dll 的区分),故 zip 内按平台加后缀区分,
|
||||||
|
// 解包时挑当前平台那一份重命名为 plugin.bin。
|
||||||
func platformBinary() (zipName, canonicalName string) {
|
func platformBinary() (zipName, canonicalName string) {
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "linux":
|
case "linux", "darwin", "windows", "freebsd":
|
||||||
return "plugin.so", "plugin.so"
|
return fmt.Sprintf("plugin.bin.%s.%s", runtime.GOOS, runtime.GOARCH), "plugin.bin"
|
||||||
case "darwin":
|
|
||||||
return "plugin.dylib", "plugin.so" // dlopen 兼容 .so 名称
|
|
||||||
case "windows":
|
|
||||||
return "plugin.dll", "plugin.dll"
|
|
||||||
default:
|
default:
|
||||||
return "", ""
|
return "", ""
|
||||||
}
|
}
|
||||||
@ -40,18 +40,17 @@ func platformBinary() (zipName, canonicalName string) {
|
|||||||
|
|
||||||
// validBinaries 是 .hmap 中所有可识别的文件入口(平台二进制或脚本)。
|
// validBinaries 是 .hmap 中所有可识别的文件入口(平台二进制或脚本)。
|
||||||
var validBinaries = map[string]bool{
|
var validBinaries = map[string]bool{
|
||||||
"plugin.so": true,
|
"plugin.bin": true,
|
||||||
"plugin.dylib": true,
|
"main.lua": true,
|
||||||
"plugin.dll": true,
|
"SKILL.md": true,
|
||||||
"main.lua": true,
|
|
||||||
"SKILL.md": true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// platformBinaries 是平台特定的二进制,bundle 模式下仅当前平台的被解压。
|
// isPlatformBinary 判断 zip 条目是否为平台特定二进制(bundle 模式下仅当前平台的被解压)。
|
||||||
var platformBinaries = map[string]bool{
|
//
|
||||||
"plugin.so": true,
|
// 形式:plugin.bin.<goos>.<goarch>。不用固定表是因为平台组合会增长
|
||||||
"plugin.dylib": true,
|
// (linux/arm64、darwin/arm64 等),按前缀判断无需维护清单。
|
||||||
"plugin.dll": true,
|
func isPlatformBinary(name string) bool {
|
||||||
|
return strings.HasPrefix(name, "plugin.bin.")
|
||||||
}
|
}
|
||||||
|
|
||||||
var downloadClient = &http.Client{
|
var downloadClient = &http.Client{
|
||||||
@ -705,17 +704,19 @@ func validatePackage(data []byte) (*pluginPackage, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(pkg.Platforms) > 0 {
|
if len(pkg.Platforms) > 0 {
|
||||||
// bundle mode: check each declared platform has a matching binary
|
// bundle mode:每个声明的平台都要有对应二进制。
|
||||||
|
// 子进程模式下条目形式为 plugin.bin.<goos>.<goarch>,
|
||||||
|
// 故按前缀匹配而不枚举架构(同一 OS 可能有 amd64/arm64 两份)。
|
||||||
for _, plat := range pkg.Platforms {
|
for _, plat := range pkg.Platforms {
|
||||||
bin, ok := map[string]string{
|
prefix := "plugin.bin." + plat + "."
|
||||||
"linux": "plugin.so",
|
found := false
|
||||||
"darwin": "plugin.dylib",
|
for name := range zipEntries {
|
||||||
"windows": "plugin.dll",
|
if strings.HasPrefix(name, prefix) {
|
||||||
}[plat]
|
found = true
|
||||||
if !ok {
|
break
|
||||||
return nil, fmt.Errorf("unsupported platform: %q", plat)
|
}
|
||||||
}
|
}
|
||||||
if zipEntries[bin] {
|
if found {
|
||||||
hasBinary = true
|
hasBinary = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -795,11 +796,11 @@ func extractPackage(data []byte, pluginDir string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// bundle mode: skip other platforms' platform-specific binaries
|
// 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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// rename platform binary to canonical name (e.g. plugin.dylib → plugin.so)
|
// 平台二进制重命名为规范名(plugin.bin.linux.amd64 → plugin.bin)
|
||||||
dest := fpath
|
dest := fpath
|
||||||
if isBundle && f.Name == zipBin && canonicalName != zipBin {
|
if isBundle && f.Name == zipBin && canonicalName != zipBin {
|
||||||
dest = filepath.Join(target, canonicalName)
|
dest = filepath.Join(target, canonicalName)
|
||||||
@ -808,6 +809,15 @@ func extractPackage(data []byte, pluginDir string) error {
|
|||||||
if err := copyZipEntry(f, dest); err != nil {
|
if err := copyZipEntry(f, dest); err != nil {
|
||||||
return err
|
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
|
return nil
|
||||||
|
|||||||
@ -89,12 +89,12 @@ func buildHmap(t *testing.T, name, version string) []byte {
|
|||||||
zw := zip.NewWriter(&buf)
|
zw := zip.NewWriter(&buf)
|
||||||
manifest := map[string]interface{}{
|
manifest := map[string]interface{}{
|
||||||
"name": name, "name_zh": name, "name_en": name,
|
"name": name, "name_zh": name, "name_en": name,
|
||||||
"version": version, "entry": "plugin.so",
|
"version": version, "entry": "plugin.bin",
|
||||||
}
|
}
|
||||||
mData, _ := json.Marshal(manifest)
|
mData, _ := json.Marshal(manifest)
|
||||||
f, _ := zw.Create("plugin.json")
|
f, _ := zw.Create("plugin.json")
|
||||||
f.Write(mData)
|
f.Write(mData)
|
||||||
bin, _ := zw.Create("plugin.so")
|
bin, _ := zw.Create("plugin.bin")
|
||||||
bin.Write([]byte("binary-" + name + "-" + version))
|
bin.Write([]byte("binary-" + name + "-" + version))
|
||||||
zw.Close()
|
zw.Close()
|
||||||
return buf.Bytes()
|
return buf.Bytes()
|
||||||
@ -152,12 +152,12 @@ func TestInstallThenUpgradeKeepsConfig(t *testing.T) {
|
|||||||
t.Fatalf("StopAndUnload not called once with demo: %v", calls)
|
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 {
|
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" {
|
if string(binData) != "binary-demo-2.0.0" {
|
||||||
t.Fatalf("so not overwritten: %q", string(soData))
|
t.Fatalf("bin not overwritten: %q", string(binData))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 降级 v2.0.0 → v1.5.0
|
// 4. 降级 v2.0.0 → v1.5.0
|
||||||
@ -187,7 +187,7 @@ func TestExtractFailureRollsBack(t *testing.T) {
|
|||||||
// 构造损坏包:zip 但缺 plugin.json(extractPackage 会失败)
|
// 构造损坏包:zip 但缺 plugin.json(extractPackage 会失败)
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
zw := zip.NewWriter(&buf)
|
zw := zip.NewWriter(&buf)
|
||||||
f, _ := zw.Create("plugin.so")
|
f, _ := zw.Create("plugin.bin")
|
||||||
f.Write([]byte("corrupt"))
|
f.Write([]byte("corrupt"))
|
||||||
zw.Close()
|
zw.Close()
|
||||||
|
|
||||||
@ -200,7 +200,7 @@ func TestExtractFailureRollsBack(t *testing.T) {
|
|||||||
f2, _ := zw2.Create("../../evil")
|
f2, _ := zw2.Create("../../evil")
|
||||||
f2.Write([]byte("x"))
|
f2.Write([]byte("x"))
|
||||||
mf, _ := zw2.Create("plugin.json")
|
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)
|
mf.Write(mData)
|
||||||
zw2.Close()
|
zw2.Close()
|
||||||
bad = rb.Bytes()
|
bad = rb.Bytes()
|
||||||
|
|||||||
1296
third_party/homeagent-sdk/tools/plugindev/templates.go
vendored
1296
third_party/homeagent-sdk/tools/plugindev/templates.go
vendored
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user