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