mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
Part 1 加载分派骨架(迁移可逐插件推进、随时回退的前提):
- dynamic.go: 新增 binEntry/skillEntry 常量 + entryKind 枚举 + classifyEntry/detectEntryKind
manifest entry 优先级最高(改回 plugin.so 即回退 cabi);无 manifest 时按目录探测,.bin 优先
- registry.go: tryDynamic 按 entry 分派 proc/cabi 双通道;
entry 声明 .bin 但二进制缺失时报明确错误,不静默回退(否则'已迁移插件跑回旧通道'极难排查)
- registry.go: pluginEntryHash 候选顺序与 detectEntryKind 对齐(.bin 优先),
否则增量重载会用错文件算 hash
- dynamic_proc_{unix,windows}.go: tryLoadProc 桩位(权限/类型校验已实现,进程管理属 Part 2)
Part 4 共享内存数据面(迁移评估 §3.3/§3.4/§3.7,最关键一环):
- proc/shm.go: 段布局(Header + ShmStageCtx 描述符数组 + append-only arena)
相对偏移设计——各进程 mmap 到不同虚拟地址仍能正确解引用
arena 用尽显式报错而非静默截断(§4.4 风险登记);Compact() 回收 append-only 垃圾
- proc/shmcodec.go: StageContext 16 字段跨进程编解码
字段级描述符消除 lost update:只改 FinalText 的插件不触碰 ToolResults 描述符
WriteDirty 只写脏字段——只读插件零写入,不可能覆盖他人改写
Snapshot 存序列化字符串(切片共享底层数组的坑,C ABI 侧修 11.3 时已踩过)
Extra 4 键提升为具名字段;Response 用标志位表达 nil vs 空串
- proc/lock.go: 锁仲裁回归内核(§3.7 已裁定,零 cgo)
ForceRelease 实现实验 9 的崩溃自愈——排除 robust pthread_mutex 必要性
重复加锁显式拒绝(否则死锁 30s);等待超时有补偿 goroutine 防锁泄漏
验证:
- proc 包 16 项测试全绿(含 -race):全字段往返/只读零写回/原地改切片识别/
现网 sanitizer+weather 场景/5插件×40轮并发零丢失/arena 耗尽报错/压实不破坏字段/
锁互斥·串扰拒绝·崩溃自愈·临界区串行化
- entry 分派 9 项测试全绿;go build ./... exit 0;接口冻结 git diff sdk/ 为空
101 lines
2.5 KiB
Go
101 lines
2.5 KiB
Go
package plugin
|
||
|
||
import (
|
||
"encoding/json"
|
||
"os"
|
||
"path/filepath"
|
||
)
|
||
|
||
const (
|
||
soEntry = "plugin.so"
|
||
dllEntry = "plugin.dll"
|
||
binEntry = "plugin.bin" // 子进程插件(纯 Go 二进制,stdio JSON-RPC)
|
||
luaEntry = "main.lua"
|
||
skillEntry = "SKILL.md"
|
||
metaEntry = "plugin.json"
|
||
)
|
||
|
||
// entryKind 描述插件入口归属的加载通道。
|
||
// 外部插件多进程化期间 .so/.dll(cabi)与 .bin(proc)**双通道共存**,
|
||
// 按 plugin.json 的 entry 字段分派,使迁移可逐插件推进、随时回退。
|
||
type entryKind int
|
||
|
||
const (
|
||
entryUnknown entryKind = iota
|
||
entryCABI // plugin.so / plugin.dll / plugin.dylib —— C ABI 动态库
|
||
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:
|
||
return "lua"
|
||
case entrySkill:
|
||
return "skill"
|
||
}
|
||
return "unknown"
|
||
}
|
||
|
||
// classifyEntry 把 manifest 的 entry 字段映射到加载通道。
|
||
// entry 为空时返回 entryUnknown,由调用方回退到目录探测(兼容无 manifest 的旧插件)。
|
||
func classifyEntry(entry string) entryKind {
|
||
switch entry {
|
||
case soEntry, dllEntry, "plugin.dylib":
|
||
return entryCABI
|
||
case binEntry:
|
||
return entryProc
|
||
case luaEntry:
|
||
return entryLua
|
||
case skillEntry:
|
||
return entrySkill
|
||
}
|
||
return entryUnknown
|
||
}
|
||
|
||
// detectEntryKind 先读 manifest 的 entry,读不到则按目录内存在的入口文件推断。
|
||
// 推断顺序:.bin 优先于 .so——迁移期间同一插件目录可能两个产物共存(升级未清理),
|
||
// 此时应走新通道;manifest 显式声明优先级最高。
|
||
func detectEntryKind(plgDir string) entryKind {
|
||
if mft := readManifest(plgDir); mft != nil {
|
||
if k := classifyEntry(mft.Entry); k != entryUnknown {
|
||
return k
|
||
}
|
||
}
|
||
for _, probe := range []struct {
|
||
file string
|
||
kind entryKind
|
||
}{
|
||
{binEntry, entryProc},
|
||
{soEntry, entryCABI},
|
||
{"plugin.dylib", entryCABI},
|
||
{dllEntry, entryCABI},
|
||
{luaEntry, entryLua},
|
||
{skillEntry, entrySkill},
|
||
} {
|
||
if st, err := os.Stat(filepath.Join(plgDir, probe.file)); err == nil && !st.IsDir() {
|
||
return probe.kind
|
||
}
|
||
}
|
||
return entryUnknown
|
||
}
|
||
|
||
func readManifest(dir string) *PluginManifest {
|
||
data, err := os.ReadFile(filepath.Join(dir, metaEntry))
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
var m PluginManifest
|
||
if err := json.Unmarshal(data, &m); err != nil {
|
||
return nil
|
||
}
|
||
return &m
|
||
}
|
||
|
||
var _ = json.Marshal
|