diff --git a/internal/plugin/proc/corehandler.go b/internal/plugin/proc/corehandler.go new file mode 100644 index 0000000..d930b93 --- /dev/null +++ b/internal/plugin/proc/corehandler.go @@ -0,0 +1,697 @@ +package proc + +import ( + "context" + "encoding/json" + "fmt" + + pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk" +) + +// coreHandler 把插件发来的 RPC 调用路由到内核 PluginSDK。 +// +// 这是 cabi/loader.go 那 51 个 case 体的**整块平移**(§3.2): +// 参数解析、调用、错误处理逻辑不变,只把「整数 method id + 三个 C 字符串槽」 +// 换成「method 名 + 结构化 JSON 参数」。语义不变,回归风险最小。 +// +// 平移带来的直接改善: +// - 参数不再挤进 s1/s2/s3 + i1/i2 五个固定槽(C ABI 的形状约束) +// - 不需要 CORE_FREE_STRING:跨进程各自 GC +// - 错误可携带结构化信息,不只是一个字符串 +type coreHandler struct { + // sdk 是内核为该插件构建的 PluginSDK(与内置插件同一类型)。 + sdk CoreSDK + // name 是插件名,用于工具归属推断与日志。 + name string + + // host 持有被全部插件共享的 StageContext 段与锁仲裁(§3.3/§3.7)。 + // ❗ 必须是"全部插件共享一个 Host"——每插件一段会退化成副本模型。 + host *Host + + // locks 是 host.locks 的引用,供 stage.lock/unlock 路由。 + locks *lockRegistry + + // invokeTool/invokeStageFn/invokeOutput 反向调用插件(内核 → 插件)。 + // 由 Plugin 注入,注册回调时用它们构造 handler。 + invokeTool func(name string, args map[string]interface{}) (interface{}, error) + invokeStageFn func(ctx context.Context, stage string, seq uint64) error + invokeOutput func(channel string, args map[string]interface{}) (interface{}, error) +} + +// invokeStageWithCtx 反向调用插件执行 stage。 +func (h *coreHandler) invokeStageWithCtx(ctx context.Context, stage string, seq uint64) error { + if h.invokeStageFn == nil { + return fmt.Errorf("插件 %s: stage 调用通道未就绪", h.name) + } + return h.invokeStageFn(ctx, stage, seq) +} + +// CoreSDK 是 coreHandler 依赖的内核能力面。 +// +// 定义为接口而非直接依赖 internal/sdk.PluginSDK,原因: +// 1. 避免 internal/plugin/proc → internal/sdk 的强耦合(后者已依赖 internal/plugin 的类型) +// 2. 单测可注入假实现,无需构造完整内核 +// +// 方法集**刻意只包含外部插件应得的能力**——`Selftest`/`Supervisor`/`Tracker`/ +// `Status`/`Adapter`/`Config`/`Tool`/`Indexer`/`OutputChan`/`Publish` 不在此列。 +// 这正是把权限梯度从「C ABI 表达能力的意外产物」变成「显式声明并强制的策略」(§3.8)。 +type CoreSDK interface { + PluginName() string + + Settings() pubsdk.SettingsAPI + Memory() pubsdk.MemoryAPI + TextMemory() pubsdk.TextMemoryAPI + DocMemory() pubsdk.DocMemoryAPI + Knowledge() pubsdk.KnowledgeAPI + LLM() pubsdk.LLMAPI + Social() pubsdk.SocialAPI + PluginMgr() pubsdk.PluginMgrAPI + + RegisterTool(name string, def pubsdk.ToolDef, handler pubsdk.ToolHandler) error + RegisterStage(stage pubsdk.Stage, handler pubsdk.StageHandler, scope ...pubsdk.StageScope) + RegisterPluginAPI(name string) error + RegisterOutputChannel(name string, caps int, desc string, def pubsdk.ChannelDef, handler pubsdk.ToolHandler) error + RegisterInputChannel(name string, def pubsdk.ChannelDef) error + + InjectText(source, channel, text string) + InjectInterruptText(source, channel, text string) + InjectTextNoMemory(source, channel, text string) + InjectInputSync(source, channel, text string) string + + SetAutoRestart(enabled bool) +} + +// Handle 分派一次插件 → 内核的调用。 +func (h *coreHandler) Handle(method string, params json.RawMessage) (interface{}, error) { + switch method { + + // ---- 注册面(原 case 1/2/3/4/46)---- + case MethodToolRegister: + return h.toolRegister(params) + case MethodStageRegister: + return h.stageRegister(params) + case MethodOutputRegister: + return h.outputRegister(params) + case MethodAPIRegister: + var p struct { + Name string `json:"name"` + } + if err := unmarshal(params, &p); err != nil { + return nil, err + } + return nil, h.sdk.RegisterPluginAPI(p.Name) + case MethodInputRegister: + var p struct { + Name string `json:"name"` + Def pubsdk.ChannelDef `json:"def"` + } + if err := unmarshal(params, &p); err != nil { + return nil, err + } + // 注意 ChannelDef.Cleaner 是函数,无法跨进程传递(§3.5 回调型资源)。 + // NoMemory 可传;Cleaner 若插件需要,须在插件侧对文本预处理后再注入。 + return nil, h.sdk.RegisterInputChannel(p.Name, pubsdk.ChannelDef{NoMemory: p.Def.NoMemory}) + + // ---- IO 注入(原 case 5/6/7/47)---- + case MethodIOInjectText: + var p injectParams + if err := unmarshal(params, &p); err != nil { + return nil, err + } + h.sdk.InjectText(p.Source, p.Channel, p.Text) + return nil, nil + case MethodIOInjectInterrupt: + var p injectParams + if err := unmarshal(params, &p); err != nil { + return nil, err + } + h.sdk.InjectInterruptText(p.Source, p.Channel, p.Text) + return nil, nil + case MethodIOInjectTextNoMem: + var p injectParams + if err := unmarshal(params, &p); err != nil { + return nil, err + } + h.sdk.InjectTextNoMemory(p.Source, p.Channel, p.Text) + return nil, nil + case MethodIOInjectSync: + var p injectParams + if err := unmarshal(params, &p); err != nil { + return nil, err + } + return map[string]interface{}{"reply": h.sdk.InjectInputSync(p.Source, p.Channel, p.Text)}, nil + + // ---- 生命周期(原 case 8)---- + case MethodLifecycleAutoRestart: + var p struct { + Enabled bool `json:"enabled"` + } + if err := unmarshal(params, &p); err != nil { + return nil, err + } + h.sdk.SetAutoRestart(p.Enabled) + return nil, nil + + // ---- 图记忆(原 case 9/10/11/12/13)---- + case MethodMemoryRecall: + mem := h.sdk.Memory() + if mem == nil { + return nil, errUnavailable("memory") + } + var p struct { + Query []string `json:"query"` + Depth int `json:"depth"` + } + if err := unmarshal(params, &p); err != nil { + return nil, err + } + entities, relations, err := mem.Recall(p.Query, p.Depth) + if err != nil { + return nil, err + } + if entities == nil { + entities = []pubsdk.Entity{} + } + if relations == nil { + relations = []pubsdk.Relation{} + } + return map[string]interface{}{"entities": entities, "relations": relations}, nil + + case MethodMemoryCommit: + mem := h.sdk.Memory() + if mem == nil { + return nil, errUnavailable("memory") + } + var p struct { + Triples []pubsdk.Triple `json:"triples"` + } + if err := unmarshal(params, &p); err != nil { + return nil, err + } + return nil, mem.Commit(p.Triples) + + case MethodMemoryIntrospect: + mem := h.sdk.Memory() + if mem == nil { + return nil, errUnavailable("memory") + } + return mem.Introspect() + + case MethodMemoryMerge: + mem := h.sdk.Memory() + if mem == nil { + return nil, errUnavailable("memory") + } + var p struct { + Source string `json:"source"` + Target string `json:"target"` + } + if err := unmarshal(params, &p); err != nil { + return nil, err + } + n, err := mem.MergeEntities(p.Source, p.Target) + if err != nil { + return nil, err + } + return map[string]interface{}{"merged": n}, nil + + case MethodMemoryPurge: + mem := h.sdk.Memory() + if mem == nil { + return nil, errUnavailable("memory") + } + var p struct { + Criteria map[string]string `json:"criteria"` + Mode string `json:"mode"` + } + if err := unmarshal(params, &p); err != nil { + return nil, err + } + if p.Mode == "" { + p.Mode = "soft" + } + n, err := mem.Purge(p.Criteria, p.Mode) + if err != nil { + return nil, err + } + return map[string]interface{}{"purged": n}, nil + + // ---- 文档记忆(原 case 14/32/33/34)---- + case MethodDocQuery: + dm := h.sdk.DocMemory() + if dm == nil { + return nil, errUnavailable("doc memory") + } + var p struct { + Text string `json:"text"` + TopK int `json:"top_k"` + } + if err := unmarshal(params, &p); err != nil { + return nil, err + } + docs := dm.Query(p.Text, p.TopK) + if docs == nil { + docs = []*pubsdk.Doc{} + } + return map[string]interface{}{"docs": docs}, nil + + case MethodDocInsert: + dm := h.sdk.DocMemory() + if dm == nil { + return nil, errUnavailable("doc memory") + } + var p struct { + Doc *pubsdk.Doc `json:"doc"` + } + if err := unmarshal(params, &p); err != nil { + return nil, err + } + if p.Doc == nil { + return nil, fmt.Errorf("doc.insert: 缺少 doc 字段") + } + return nil, dm.Insert(p.Doc) + + case MethodDocRemove: + dm := h.sdk.DocMemory() + if dm == nil { + return nil, errUnavailable("doc memory") + } + var p struct { + ID string `json:"id"` + } + if err := unmarshal(params, &p); err != nil { + return nil, err + } + dm.Remove(p.ID) + return nil, nil + + case MethodDocStats: + dm := h.sdk.DocMemory() + if dm == nil { + return nil, errUnavailable("doc memory") + } + return dm.Stats(), nil + + // ---- 知识库(原 case 15/35/36)---- + case MethodKnowledgeSearch: + kn := h.sdk.Knowledge() + if kn == nil { + return nil, errUnavailable("knowledge") + } + var p struct { + Query string `json:"query"` + TopK int `json:"top_k"` + } + if err := unmarshal(params, &p); err != nil { + return nil, err + } + results, err := kn.Search(p.Query, p.TopK) + if err != nil { + return nil, err + } + if results == nil { + results = []*pubsdk.Knowledge{} + } + return map[string]interface{}{"results": results}, nil + + case MethodKnowledgeAdd: + kn := h.sdk.Knowledge() + if kn == nil { + return nil, errUnavailable("knowledge") + } + var p struct { + Name string `json:"name"` + Content string `json:"content"` + } + if err := unmarshal(params, &p); err != nil { + return nil, err + } + return nil, kn.Add(p.Name, p.Content) + + case MethodKnowledgeList: + kn := h.sdk.Knowledge() + if kn == nil { + return nil, errUnavailable("knowledge") + } + names, err := kn.List() + if err != nil { + return nil, err + } + if names == nil { + names = []string{} + } + return map[string]interface{}{"names": names}, nil + + // ---- 文本记忆(原 case 41)---- + case MethodTextMemoryAppend: + tm := h.sdk.TextMemory() + if tm == nil { + return nil, errUnavailable("text memory") + } + var p struct { + Event pubsdk.TextEvent `json:"event"` + } + if err := unmarshal(params, &p); err != nil { + return nil, err + } + return nil, tm.Append(p.Event) + + // ---- 设置(原 case 16/17/18/26~31/42~45/51)---- + case MethodSettingsGet, MethodSettingsSet, MethodSettingsRegisterDef, + MethodSettingsGetCore, MethodSettingsSetCore, MethodSettingsListCore, + MethodSettingsGetPlugin, MethodSettingsSetPlugin, MethodSettingsListPlugin, + MethodSettingsList, MethodSettingsDefs, MethodSettingsDump, + MethodSettingsPlugins, MethodSettingsDataDir: + return h.settings(method, params) + + // ---- LLM 源(原 case 19/20/37)---- + case MethodLLMListSources: + llm := h.sdk.LLM() + if llm == nil { + return nil, errUnavailable("llm") + } + sources := llm.ListSources() + if sources == nil { + sources = []string{} + } + return map[string]interface{}{"sources": sources}, nil + case MethodLLMSetSource: + llm := h.sdk.LLM() + if llm == nil { + return nil, errUnavailable("llm") + } + var p struct { + Name string `json:"name"` + } + if err := unmarshal(params, &p); err != nil { + return nil, err + } + return nil, llm.SetSource(p.Name) + case MethodLLMCurrentSource: + llm := h.sdk.LLM() + if llm == nil { + return nil, errUnavailable("llm") + } + return map[string]interface{}{"source": llm.CurrentSource()}, nil + + // ---- 社交图(只读,原 case 21/22/38/39/40)---- + case MethodSocialGetPerson, MethodSocialGetNetwork, MethodSocialGetTrait, + MethodSocialGetRelation, MethodSocialListPersons: + return h.social(method, params) + + // ---- 插件管理(原 case 48/49/50)---- + case MethodPluginReloadOne: + pm := h.sdk.PluginMgr() + if pm == nil { + return nil, errUnavailable("plugin manager") + } + var p struct { + Name string `json:"name"` + } + if err := unmarshal(params, &p); err != nil { + return nil, err + } + return nil, pm.ReloadOne(p.Name) + case MethodPluginListLoaded: + pm := h.sdk.PluginMgr() + if pm == nil { + return nil, errUnavailable("plugin manager") + } + list := pm.ListLoadedPlugins() + if list == nil { + list = []string{} + } + return map[string]interface{}{"plugins": list}, nil + case MethodPluginIsDisabled: + pm := h.sdk.PluginMgr() + if pm == nil { + return nil, errUnavailable("plugin manager") + } + var p struct { + Name string `json:"name"` + } + if err := unmarshal(params, &p); err != nil { + return nil, err + } + return map[string]interface{}{"disabled": pm.IsPluginDisabled(p.Name)}, nil + + // ---- 共享段锁仲裁(新增,§3.7)---- + case MethodStageLock: + if h.locks == nil { + return nil, fmt.Errorf("stage.lock: 锁仲裁未就绪") + } + return nil, h.locks.acquire(h.name) + case MethodStageUnlock: + if h.locks == nil { + return nil, fmt.Errorf("stage.unlock: 锁仲裁未就绪") + } + return nil, h.locks.release(h.name) + + // ---- 事件订阅(原 case 23/24,今日空实现)---- + case MethodEventsSubscribe, MethodEventsUnsubscribe: + // Part 5 通知面(事件环 + eventfd)落地后接线。 + // 今日 C ABI 侧是空实现("给不了"而非"不给",§1.3); + // 明确返回未实现,比静默成功后收不到事件更容易排查。 + return nil, fmt.Errorf("%s: 事件订阅待 Part 5 通知面落地(事件环 + eventfd)", method) + + // ---- 多模态注入(C ABI 侧空实现)---- + case MethodIOSetToolBlocks: + // Part 4 扩展:二进制落 arena、Slice 描述符回传(§3.8)。 + return nil, fmt.Errorf("%s: 多模态注入待共享段二进制通道落地", method) + } + + return nil, fmt.Errorf("未知 method: %s", method) +} + +type injectParams struct { + Source string `json:"source"` + Channel string `json:"channel"` + Text string `json:"text"` +} + +func unmarshal(params json.RawMessage, out interface{}) error { + if len(params) == 0 { + return nil + } + if err := json.Unmarshal(params, out); err != nil { + return fmt.Errorf("参数解析失败: %w", err) + } + return nil +} + +func errUnavailable(what string) error { + return fmt.Errorf("%s 能力在当前内核实例中不可用", what) +} + +// toolRegister 注册插件工具,handler 反向调用插件执行(原 case 1)。 +func (h *coreHandler) toolRegister(params json.RawMessage) (interface{}, error) { + var p struct { + Name string `json:"name"` + Def pubsdk.ToolDef `json:"def"` + } + if err := unmarshal(params, &p); err != nil { + return nil, err + } + if p.Name == "" { + return nil, fmt.Errorf("tool.register: 缺少 name") + } + p.Def.Plugin = h.name + // ToolDef.Cleaner 是函数,跨进程无法传递(§3.5)——与 C ABI 路径行为一致。 + p.Def.Cleaner = nil + + name := p.Name + return nil, h.sdk.RegisterTool(name, p.Def, func(args map[string]interface{}) (interface{}, error) { + return h.invokeTool(name, args) + }) +} + +// stageRegister 注册阶段处理器(原 case 2)。 +// +// **与 C ABI 路径的本质差异**:这里不做「快照 → 副本 → 写回」。 +// StageContext 的数据在共享段,插件直接在同一份状态上读改写, +// 由锁仲裁串行化——消除了副本模型的 lost update(§8.4 实测 35.8~36.8%)。 +func (h *coreHandler) stageRegister(params json.RawMessage) (interface{}, error) { + var p struct { + Stage string `json:"stage"` + Scope string `json:"scope"` + } + if err := unmarshal(params, &p); err != nil { + return nil, err + } + if p.Stage == "" { + return nil, fmt.Errorf("stage.register: 缺少 stage") + } + + scope := pubsdk.StageScopeGlobal + if p.Scope == "own_tools" { + scope = pubsdk.StageScopeOwnTools + } + stage := p.Stage + h.sdk.RegisterStage(pubsdk.Stage(stage), func(sc *pubsdk.StageContext) error { + return h.runStage(stage, sc) + }, scope) + return nil, nil +} + +// outputRegister 注册输出通道(原 case 3)。 +// +// **与 C ABI 路径的本质差异**:可同步等真实结果。 +// C ABI 下因 cgo 不可嵌套,只能异步 fire-and-forget,导致 output_send +// 永远返回成功(§9.4,现网 2 次消息发不出而模型以为成功)。 +func (h *coreHandler) outputRegister(params json.RawMessage) (interface{}, error) { + var p struct { + Name string `json:"name"` + Caps int `json:"caps"` + Desc string `json:"desc"` + Def pubsdk.ChannelDef `json:"def"` + } + if err := unmarshal(params, &p); err != nil { + return nil, err + } + if p.Name == "" { + return nil, fmt.Errorf("output.register: 缺少 name") + } + channel := p.Name + return nil, h.sdk.RegisterOutputChannel(channel, p.Caps, p.Desc, + pubsdk.ChannelDef{NoMemory: p.Def.NoMemory}, + func(args map[string]interface{}) (interface{}, error) { + return h.invokeOutput(channel, args) + }) +} + +func (h *coreHandler) settings(method string, params json.RawMessage) (interface{}, error) { + sett := h.sdk.Settings() + if sett == nil { + return nil, errUnavailable("settings") + } + var p struct { + Key string `json:"key"` + Value interface{} `json:"value"` + Prefix string `json:"prefix"` + Plugin string `json:"plugin"` + Def pubsdk.ConfigDef `json:"def"` + } + if err := unmarshal(params, &p); err != nil { + return nil, err + } + + switch method { + case MethodSettingsGet: + v, err := sett.Get(p.Key) + if err != nil { + return nil, err + } + return map[string]interface{}{"value": v}, nil + case MethodSettingsSet: + return nil, sett.Set(p.Key, p.Value) + case MethodSettingsRegisterDef: + sett.RegisterDef(p.Def) + return nil, nil + case MethodSettingsGetCore: + v, err := sett.GetCore(p.Key) + if err != nil { + return nil, err + } + return map[string]interface{}{"value": v}, nil + case MethodSettingsSetCore: + return nil, sett.SetCore(p.Key, p.Value) + case MethodSettingsListCore: + keys, err := sett.ListCore(p.Prefix) + if err != nil { + return nil, err + } + return map[string]interface{}{"keys": orEmpty(keys)}, nil + case MethodSettingsGetPlugin: + v, err := sett.GetPlugin(p.Plugin, p.Key) + if err != nil { + return nil, err + } + return map[string]interface{}{"value": v}, nil + case MethodSettingsSetPlugin: + return nil, sett.SetPlugin(p.Plugin, p.Key, p.Value) + case MethodSettingsListPlugin: + keys, err := sett.ListPlugin(p.Plugin, p.Prefix) + if err != nil { + return nil, err + } + return map[string]interface{}{"keys": orEmpty(keys)}, nil + case MethodSettingsList: + keys, err := sett.List(p.Prefix) + if err != nil { + return nil, err + } + return map[string]interface{}{"keys": orEmpty(keys)}, nil + case MethodSettingsDefs: + defs := sett.Defs(p.Prefix) + if defs == nil { + defs = []*pubsdk.ConfigDef{} + } + return map[string]interface{}{"defs": defs}, nil + case MethodSettingsDump: + return sett.Dump(), nil + case MethodSettingsPlugins: + return map[string]interface{}{"plugins": orEmpty(sett.Plugins())}, nil + case MethodSettingsDataDir: + return map[string]interface{}{"dir": sett.DataDir()}, nil + } + return nil, fmt.Errorf("未知 settings method: %s", method) +} + +func (h *coreHandler) social(method string, params json.RawMessage) (interface{}, error) { + social := h.sdk.Social() + if social == nil { + return nil, errUnavailable("social") + } + var p struct { + Name string `json:"name"` + Trait string `json:"trait"` + Depth int `json:"depth"` + } + if err := unmarshal(params, &p); err != nil { + return nil, err + } + + switch method { + case MethodSocialGetPerson: + profile, err := social.GetPerson(p.Name) + if err != nil { + return nil, err + } + return map[string]interface{}{"person": profile}, nil + case MethodSocialGetNetwork: + profiles, err := social.GetNetwork(p.Name, p.Depth) + if err != nil { + return nil, err + } + if profiles == nil { + profiles = []*pubsdk.PersonProfile{} + } + return map[string]interface{}{"network": profiles}, nil + case MethodSocialGetTrait: + v, ok := social.GetTrait(p.Name, p.Trait) + return map[string]interface{}{"value": v, "found": ok}, nil + case MethodSocialGetRelation: + rels, err := social.GetRelations(p.Name) + if err != nil { + return nil, err + } + if rels == nil { + rels = []pubsdk.SocialRelation{} + } + return map[string]interface{}{"relations": rels}, nil + case MethodSocialListPersons: + names, err := social.ListPersons() + if err != nil { + return nil, err + } + return map[string]interface{}{"persons": orEmpty(names)}, nil + } + return nil, fmt.Errorf("未知 social method: %s", method) +} + +func orEmpty(s []string) []string { + if s == nil { + return []string{} + } + return s +} diff --git a/internal/plugin/proc/host.go b/internal/plugin/proc/host.go new file mode 100644 index 0000000..d76feb5 --- /dev/null +++ b/internal/plugin/proc/host.go @@ -0,0 +1,215 @@ +package proc + +import ( + "fmt" + "log" + "os" + "sync" + + pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk" + "golang.org/x/sys/unix" +) + +// Host 持有**被全部子进程插件共享的一块 StageContext 段**,是共享内存数据面的 +// 所有权中心(§3.3/§3.4)。 +// +// ❗ 为什么必须共享一块段(这是一个容易走错的关键点): +// 若每个插件各持一块段,则「内核 ctx → 段 → 插件改 → 回读 ctx」在多插件下退化成 +// 副本模型——两个插件各写各的段、各自回读,最后回读者覆盖前者, +// lost update 原样复现(§8.4 实测 35.8~36.8%)。 +// 实验 8 的做法是 5 个 worker 进程 mmap **同一个 memfd**,本实现与之一致。 +// +// 生命周期:Host 由 registry 创建一次,随内核存活;每个插件 spawn 时经 +// ExtraFiles 拿到同一 memfd(fd 3),mmap 后即看到同一份物理页。 +type Host struct { + memfd *os.File + data []byte + seg *Segment + shmSize int + + // locks 被全部插件的 coreHandler 共享——同阶段并发扇出的插件在此排队, + // 语义等价于内置插件共享 *StageContext 的 sync.RWMutex(§0.2 第 1 条)。 + locks *lockRegistry + + // stageMu 串行化「整次 stage 执行」对共享段的独占。 + // + // 必要性:内核可能在不同路径并发触发 RunStage(如 emitResponse 的 + // before_output 与主循环的其他阶段)。段只有一份,两次 stage 交叠会互相污染。 + // 由首个进入的插件加锁、最后离开的插件解锁;RunStage 的 wg.Wait() 保证 + // 每个 handler 的 defer 必然执行,故 inflight 必然归零,不会死锁。 + stageMu sync.Mutex + + coordMu sync.Mutex + coord *stageCoordinator +} + +// NewHost 创建共享段(memfd + mmap + 布局初始化)。 +// +// 用 memfd 而非 /dev/shm 文件:无需文件名、不残留(进程退出即回收)、 +// 可经 ExtraFiles 传给子进程。实验 2 已验证父子 mmap 到不同虚拟地址时 +// 相对偏移仍正确解引用。 +func NewHost() (*Host, error) { + fd, err := unix.MemfdCreate("hastagectx", unix.MFD_CLOEXEC) + if err != nil { + return nil, fmt.Errorf("proc: 创建共享段 memfd: %w", err) + } + if err := unix.Ftruncate(fd, int64(shmDefaultSize)); err != nil { + unix.Close(fd) + return nil, fmt.Errorf("proc: 共享段 ftruncate: %w", err) + } + data, err := unix.Mmap(fd, 0, shmDefaultSize, + unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED) + if err != nil { + unix.Close(fd) + return nil, fmt.Errorf("proc: 共享段 mmap: %w", err) + } + seg, err := NewSegment(data) + if err != nil { + unix.Munmap(data) + unix.Close(fd) + return nil, err + } + + return &Host{ + memfd: os.NewFile(uintptr(fd), "hastagectx"), + data: data, + seg: seg, + shmSize: shmDefaultSize, + locks: &lockRegistry{}, + }, nil +} + +// shmDefaultSize 是共享 StageContext 段的大小。 +// +// 取 256KB:StageContext 全字段 JSON 化后典型 < 4KB(工具结果中位 93B,§2.5), +// append-only 中间垃圾由 stage 结束时 Compact 回收,256KB 给足余量。 +// 全部插件共享一块,总开销恒定,不随插件数增长。 +const shmDefaultSize = 256 * 1024 + +// Close 释放共享段。 +func (h *Host) Close() error { + if h.data != nil { + unix.Munmap(h.data) + h.data = nil + } + if h.memfd != nil { + err := h.memfd.Close() + h.memfd = nil + return err + } + return nil +} + +// beginStage 由插件 handler 进入时调用。 +// +// 首个进入者:获取 stageMu(独占共享段)→ 把内核 StageContext 写入段。 +// 后续进入者:仅递增 inflight。 +func (h *Host) beginStage(sc *pubsdk.StageContext) (*stageCoordinator, error) { + h.coordMu.Lock() + first := h.coord == nil + if first { + // 独占共享段直到本次 stage 全部插件离开 + h.coordMu.Unlock() + h.stageMu.Lock() + h.coordMu.Lock() + // 双检:等锁期间可能已有其他插件建好协调器(它们会先拿到 stageMu) + if h.coord != nil { + first = false + h.stageMu.Unlock() + } else { + h.coord = newStageCoordinator(h.seg) + } + } + coord := h.coord + h.coordMu.Unlock() + + if err := coord.enter(sc, first); err != nil { + if first { + h.coordMu.Lock() + h.coord = nil + h.coordMu.Unlock() + h.stageMu.Unlock() + } + return nil, err + } + h.locks.bind(coord.lock) + return coord, nil +} + +// endStage 由插件 handler 返回时调用。 +// 最后离开者:把共享段结果读回内核 StageContext → 压实 arena → 释放 stageMu。 +func (h *Host) endStage(coord *stageCoordinator) error { + last, err := coord.leave() + if !last { + return err + } + h.coordMu.Lock() + h.coord = nil + h.coordMu.Unlock() + h.stageMu.Unlock() + return err +} + +// ForceReleaseLock 在插件进程崩溃时释放其可能持有的 stage 锁(实验 9 自愈机制)。 +func (h *Host) ForceReleaseLock(plugin string) bool { + return h.locks.forceRelease(plugin) +} + +// Segment 暴露共享段(供诊断与测试)。 +func (h *Host) Segment() *Segment { return h.seg } + +// stageCoordinator 跟踪一次 stage 执行中参与插件的进出。 +type stageCoordinator struct { + seg *Segment + lock *stageLock + + mu sync.Mutex + inflight int + written bool + ctxRef *pubsdk.StageContext +} + +func newStageCoordinator(seg *Segment) *stageCoordinator { + return &stageCoordinator{seg: seg, lock: newStageLock()} +} + +// enter 登记一个插件进入本次 stage;first 为真时把内核状态写入共享段。 +func (c *stageCoordinator) enter(sc *pubsdk.StageContext, first bool) error { + c.mu.Lock() + defer c.mu.Unlock() + c.inflight++ + if !first || c.written { + return nil + } + c.ctxRef = sc + if err := c.seg.WriteAll(sc); err != nil { + c.inflight-- + return fmt.Errorf("写入共享段: %w", err) + } + c.written = true + return nil +} + +// leave 登记一个插件离开;返回是否为最后一个离开者。 +// +// 最后离开者负责把共享段结果读回内核 StageContext,并压实 arena +// (此时无插件持锁,满足 §3.3 的压实前提)。 +func (c *stageCoordinator) leave() (last bool, err error) { + c.mu.Lock() + c.inflight-- + last = c.inflight == 0 + sc := c.ctxRef + written := c.written + c.mu.Unlock() + + if !last || !written || sc == nil { + return last, nil + } + if rErr := c.seg.ReadInto(sc); rErr != nil { + return last, fmt.Errorf("回读共享段: %w", rErr) + } + if reclaimed := c.seg.Compact(); reclaimed > 0 { + log.Printf("[proc] stage 结束,arena 压实回收 %d 字节", reclaimed) + } + return last, nil +} diff --git a/internal/plugin/proc/plugin.go b/internal/plugin/proc/plugin.go new file mode 100644 index 0000000..0f33b0e --- /dev/null +++ b/internal/plugin/proc/plugin.go @@ -0,0 +1,229 @@ +package proc + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + "sync" + + pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk" +) + +// Plugin 是 registry 可加载的子进程插件,与内置插件同构的启停接口。 +// +// 生命周期: +// +// New() 创建(尚未 spawn) +// Start(core) spawn 子进程 → 握手(传共享段 fd)→ plugin.init → plugin.start +// (plugin.start 期间插件反向注册工具/阶段/通道) +// Stop() plugin.stop → 宽限期 → 必要时 Kill +// Close() 强制结束(registry 卸载/重载路径) +// +// **共享段不属于 Plugin**:它属于 Host,被全部子进程插件共享。 +// 若每插件一段,「内核 ctx → 段 → 插件改 → 回读 ctx」在多插件下会退化成 +// 副本模型,lost update 原样复现(§8.4)。 +type Plugin struct { + name string + bin string + dir string + config map[string]interface{} + + host *Host + proc *Process + handler *coreHandler + + // env 追加到子进程环境变量(测试用;生产由 registry 按需设置)。 + env []string + + // onCrash 由 registry 注入,把进程退出喂给 plugin_health.recordCrash(§2.3)。 + onCrash func(name string, err error) + + stopOnce sync.Once +} + +// New 创建子进程插件(不启动进程)。 +// +// host 必须是全部子进程插件共用的实例(由 registry 创建一次)。 +func New(name, bin, dir string, config map[string]interface{}, host *Host, onCrash func(string, error)) *Plugin { + return &Plugin{ + name: name, + bin: bin, + dir: dir, + config: config, + host: host, + onCrash: onCrash, + } +} + +// Name 实现 sdk.Plugin。 +func (p *Plugin) Name() string { return p.name } + +// Start 启动子进程并完成注册。 +// +// core 是内核为该插件构建的能力面(internal/sdk.PluginSDK 天然满足 CoreSDK)。 +func (p *Plugin) Start(core CoreSDK) error { + if p.host == nil { + return fmt.Errorf("proc: %s 缺少共享段 Host", p.name) + } + + p.handler = &coreHandler{ + sdk: core, + name: p.name, + host: p.host, + locks: p.host.locks, + } + // 反向调用闭包:注册回调时捕获,运行期经 RPC 打到插件进程。 + p.handler.invokeTool = p.invokeTool + p.handler.invokeStageFn = p.invokeStage + p.handler.invokeOutput = p.invokeOutput + + proc, err := Spawn(p.name, p.bin, Options{ + Dir: p.dir, + Env: p.env, + // 子进程 fd 3 = 共享段 memfd(全部插件同一个,故看到同一份物理页) + ExtraFiles: []*os.File{p.host.memfd}, + ShmSize: p.host.shmSize, + Handler: p.handler.Handle, + OnExit: p.handleExit, + }) + if err != nil { + return err + } + p.proc = proc + + // plugin.init:构造插件实例 + if _, err := proc.Call(MethodPluginInit, PluginInitParams{ + Name: p.name, + Config: p.config, + }); err != nil { + proc.Kill() + return fmt.Errorf("proc: %s plugin.init 失败: %w", p.name, err) + } + + // plugin.start:插件在此期间反向注册工具/阶段/通道 + if _, err := proc.Call(MethodPluginStart, nil); err != nil { + proc.Kill() + return fmt.Errorf("proc: %s plugin.start 失败: %w", p.name, err) + } + return nil +} + +// Stop 优雅停止(实现 sdk.Plugin)。 +func (p *Plugin) Stop() error { + var err error + p.stopOnce.Do(func() { + if p.proc != nil { + err = p.proc.Stop() + } + }) + return err +} + +// Close 强制结束子进程。 +// +// **这里是真 kill + wait**——对比 cabi 路径的 Close 只做 dlclose, +// 而 dlclose 对 Go c-shared 是 no-op(§1.1,热重载静默失效的根因)。 +func (p *Plugin) Close() error { + var err error + p.stopOnce.Do(func() { + if p.proc != nil { + err = p.proc.Kill() + } + }) + return err +} + +// handleExit 在子进程退出时把信号喂给 plugin_health(§2.3 逻辑复用), +// 并释放该插件可能持有的 stage 锁。 +// +// 后者是"锁仲裁回内核"的自愈价值:持锁者死亡不会导致全局死锁, +// 无需 robust pthread_mutex(实验 9)。 +func (p *Plugin) handleExit(name string, err error) { + if p.host != nil && p.host.ForceReleaseLock(name) { + log.Printf("[proc] %s 退出,内核已释放其持有的 stage 锁", name) + } + if err != nil && p.onCrash != nil { + p.onCrash(name, err) + } +} + +// ---- 内核 → 插件的反向调用 ---- + +func (p *Plugin) invokeTool(name string, args map[string]interface{}) (interface{}, error) { + if p.proc == nil { + return nil, ErrProcessExited + } + raw, err := p.proc.Call(MethodToolInvoke, ToolInvokeParams{Name: name, Args: args}) + if err != nil { + return nil, err + } + var res ToolInvokeResult + if err := json.Unmarshal(raw, &res); err != nil { + return nil, fmt.Errorf("proc: %s 工具 %s 应答解析失败: %w", p.name, name, err) + } + return res.Result, nil +} + +func (p *Plugin) invokeStage(ctx context.Context, stage string, seq uint64) error { + if p.proc == nil { + return ErrProcessExited + } + raw, err := p.proc.CallContext(ctx, MethodStageInvoke, StageInvokeParams{ + Stage: stage, + Seq: seq, + }) + if err != nil { + return err + } + var res StageInvokeResult + if len(raw) > 0 { + if err := json.Unmarshal(raw, &res); err != nil { + return fmt.Errorf("proc: %s stage %s 应答解析失败: %w", p.name, stage, err) + } + } + if res.DirtyFields > 0 { + log.Printf("[proc] %s stage %s 改写了 %d 个字段", p.name, stage, res.DirtyFields) + } + return nil +} + +// invokeOutput 经插件输出通道发送,**同步等待真实结果**。 +// +// 这是 §9.4 的根治:C ABI 下 cgo 不可嵌套,只能异步 fire-and-forget, +// 导致 output_send 永远返回 {status:queued} + err=nil,模型永远以为发送成功 +// (现网 7 天内 2 次消息实际发不出)。进程模型下 RPC 天然可等应答。 +func (p *Plugin) invokeOutput(channel string, args map[string]interface{}) (interface{}, error) { + if p.proc == nil { + return nil, ErrProcessExited + } + raw, err := p.proc.Call(MethodOutputInvoke, OutputInvokeParams{ + Channel: channel, + Args: args, + }) + if err != nil { + return nil, err // 真实失败上报,模型可感知并重试 + } + if len(raw) == 0 { + return map[string]interface{}{"status": "sent"}, nil + } + var res map[string]interface{} + if err := json.Unmarshal(raw, &res); err != nil { + return map[string]interface{}{"status": "sent"}, nil + } + if _, ok := res["status"]; !ok { + res["status"] = "sent" + } + return res, nil +} + +// 编译期确认 Plugin 具备 registry 需要的启停形状。 +var _ interface { + Name() string + Stop() error + Close() error +} = (*Plugin)(nil) + +// 引用一下公开 SDK,确保本文件的类型假设与它同版本。 +var _ = pubsdk.StageScopeGlobal diff --git a/internal/plugin/proc/plugin_test.go b/internal/plugin/proc/plugin_test.go new file mode 100644 index 0000000..4df85d6 --- /dev/null +++ b/internal/plugin/proc/plugin_test.go @@ -0,0 +1,393 @@ +package proc + +import ( + "encoding/json" + "fmt" + "strings" + "sync" + "testing" + + pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk" +) + +// 端到端验证:内核 RunStage 并发扇出 → 真实子进程插件经共享内存读改写 → 结果回读。 +// +// 这是**整个迁移最关键的一环闭环验证**(§4.4 风险 3.4): +// 机制在 shm_test.go 已被单元验证,这里验证它在真进程 + 真 RPC 下同样成立。 + +// fakeCoreSDK 是最简 CoreSDK 实现,记录注册行为。 +type fakeCoreSDK struct { + mu sync.Mutex + tools map[string]pubsdk.ToolHandler + stages map[pubsdk.Stage][]pubsdk.StageHandler + outputs map[string]pubsdk.ToolHandler + settings map[string]interface{} + autoStart bool +} + +func newFakeCore() *fakeCoreSDK { + return &fakeCoreSDK{ + tools: map[string]pubsdk.ToolHandler{}, + stages: map[pubsdk.Stage][]pubsdk.StageHandler{}, + outputs: map[string]pubsdk.ToolHandler{}, + settings: map[string]interface{}{}, + } +} + +func (f *fakeCoreSDK) PluginName() string { return "fake" } +func (f *fakeCoreSDK) Settings() pubsdk.SettingsAPI { return nil } +func (f *fakeCoreSDK) Memory() pubsdk.MemoryAPI { return nil } +func (f *fakeCoreSDK) TextMemory() pubsdk.TextMemoryAPI { return nil } +func (f *fakeCoreSDK) DocMemory() pubsdk.DocMemoryAPI { return nil } +func (f *fakeCoreSDK) Knowledge() pubsdk.KnowledgeAPI { return nil } +func (f *fakeCoreSDK) LLM() pubsdk.LLMAPI { return nil } +func (f *fakeCoreSDK) Social() pubsdk.SocialAPI { return nil } +func (f *fakeCoreSDK) PluginMgr() pubsdk.PluginMgrAPI { return nil } +func (f *fakeCoreSDK) RegisterPluginAPI(name string) error { return nil } +func (f *fakeCoreSDK) InjectText(s, c, t string) {} +func (f *fakeCoreSDK) InjectInterruptText(s, c, t string) {} +func (f *fakeCoreSDK) InjectTextNoMemory(s, c, t string) {} +func (f *fakeCoreSDK) InjectInputSync(s, c, t string) string { return "" } +func (f *fakeCoreSDK) SetAutoRestart(enabled bool) { f.autoStart = enabled } + +func (f *fakeCoreSDK) RegisterTool(name string, def pubsdk.ToolDef, h pubsdk.ToolHandler) error { + f.mu.Lock() + defer f.mu.Unlock() + f.tools[name] = h + return nil +} + +func (f *fakeCoreSDK) RegisterStage(stage pubsdk.Stage, h pubsdk.StageHandler, scope ...pubsdk.StageScope) { + f.mu.Lock() + defer f.mu.Unlock() + f.stages[stage] = append(f.stages[stage], h) +} + +func (f *fakeCoreSDK) RegisterOutputChannel(name string, caps int, desc string, def pubsdk.ChannelDef, h pubsdk.ToolHandler) error { + f.mu.Lock() + defer f.mu.Unlock() + f.outputs[name] = h + return nil +} + +func (f *fakeCoreSDK) RegisterInputChannel(name string, def pubsdk.ChannelDef) error { return nil } + +func (f *fakeCoreSDK) stageHandlers(stage pubsdk.Stage) []pubsdk.StageHandler { + f.mu.Lock() + defer f.mu.Unlock() + out := make([]pubsdk.StageHandler, len(f.stages[stage])) + copy(out, f.stages[stage]) + return out +} + +// runStageLikeKernel 复刻 internal/agent/core.StageHost.RunStage 的并发扇出语义 +// (stages.go:124 的 go func + wg.Wait),验证外部插件在同样的并发模型下正确工作。 +func runStageLikeKernel(handlers []pubsdk.StageHandler, sc *pubsdk.StageContext) []error { + var wg sync.WaitGroup + errCh := make(chan error, len(handlers)) + for _, h := range handlers { + wg.Add(1) + go func(fn pubsdk.StageHandler) { + defer wg.Done() + if err := fn(sc); err != nil { + errCh <- err + } + }(h) + } + wg.Wait() + close(errCh) + var errs []error + for err := range errCh { + errs = append(errs, err) + } + return errs +} + +// 单插件 stage 读改写:验证共享段 + RPC + 锁的完整链路。 +func TestPlugin_StageReadModifyWriteOverSharedMemory(t *testing.T) { + bin := buildTestPlugin(t, "stageplugin.go") + core := newFakeCore() + + host, err := NewHost() + if err != nil { + t.Fatalf("NewHost: %v", err) + } + defer host.Close() + + p := New("sanitizer", bin, t.TempDir(), nil, host, nil) + if err := p.Start(core); err != nil { + t.Fatalf("Start: %v", err) + } + defer p.Close() + + handlers := core.stageHandlers(pubsdk.StageAfterToolcall) + if len(handlers) != 1 { + t.Fatalf("插件应注册 1 个 after_toolcall handler,实际 %d", len(handlers)) + } + + dirty := "结果:\x1b[31m脏数据\x1b[0m" + clean := "结果:脏数据" + sc := &pubsdk.StageContext{ + Phase: pubsdk.StageAfterToolcall, + ToolResults: []pubsdk.ToolResult{{CallID: "c1", Name: "x_tool", Result: dirty}}, + } + + if errs := runStageLikeKernel(handlers, sc); len(errs) > 0 { + t.Fatalf("stage 执行失败: %v", errs) + } + + got, _ := sc.ToolResults[0].Result.(string) + if got != clean { + t.Fatalf("插件的清洗结果未回到内核 StageContext:期望 %q,实际 %q", clean, got) + } +} + +// **核心断言**:改写型插件 + 只读插件并发时,清洗结果不被覆盖。 +// 复刻现网 sanitizer + weather 场景(§8.6 实测 C ABI 下 1.6~4.3% 被覆盖)。 +func TestPlugin_ConcurrentWriterAndReaderNoLostUpdate(t *testing.T) { + bin := buildTestPlugin(t, "stageplugin.go") + + // ❗ 两个插件进程**共享同一个 Host**(同一 memfd)——这是消除 lost update 的前提。 + // 若各持一段,「内核 ctx → 段 → 插件改 → 回读 ctx」会退化成副本模型, + // 最后回读者覆盖前者,§8.4 的 35.8~36.8% 丢失原样复现。 + host, err := NewHost() + if err != nil { + t.Fatalf("NewHost: %v", err) + } + defer host.Close() + + writerCore := newFakeCore() + writer := New("sanitizer", bin, t.TempDir(), nil, host, nil) + if err := writer.Start(writerCore); err != nil { + t.Fatalf("writer Start: %v", err) + } + defer writer.Close() + + readerBin := buildTestPlugin(t, "readonlyplugin.go") + readerCore := newFakeCore() + reader := New("weather", readerBin, t.TempDir(), nil, host, nil) + if err := reader.Start(readerCore); err != nil { + t.Fatalf("reader Start: %v", err) + } + defer reader.Close() + + handlers := append( + writerCore.stageHandlers(pubsdk.StageAfterToolcall), + readerCore.stageHandlers(pubsdk.StageAfterToolcall)..., + ) + if len(handlers) != 2 { + t.Fatalf("应有 2 个 handler,实际 %d", len(handlers)) + } + + dirty := "天气:晴 \x1b[31m28°C\x1b[0m" + clean := "天气:晴 28°C" + sc := &pubsdk.StageContext{ + Phase: pubsdk.StageAfterToolcall, + ToolResults: []pubsdk.ToolResult{{CallID: "c1", Name: "weather_query", Result: dirty}}, + } + + if errs := runStageLikeKernel(handlers, sc); len(errs) > 0 { + t.Fatalf("stage 执行失败: %v", errs) + } + + got, _ := sc.ToolResults[0].Result.(string) + if got != clean { + t.Fatalf("只读插件覆盖了改写插件的清洗结果:期望 %q,实际 %q", clean, got) + } +} + +// 插件注册的工具可被内核调用,并把结果带回。 +func TestPlugin_RegisteredToolInvokable(t *testing.T) { + bin := buildTestPlugin(t, "stageplugin.go") + core := newFakeCore() + + host, err := NewHost() + if err != nil { + t.Fatalf("NewHost: %v", err) + } + defer host.Close() + + p := New("demo", bin, t.TempDir(), nil, host, nil) + if err := p.Start(core); err != nil { + t.Fatalf("Start: %v", err) + } + defer p.Close() + + core.mu.Lock() + h, ok := core.tools["demo_upper"] + core.mu.Unlock() + if !ok { + t.Fatal("插件应注册 demo_upper 工具") + } + + res, err := h(map[string]interface{}{"text": "abc"}) + if err != nil { + t.Fatalf("调用工具: %v", err) + } + if res != "ABC" { + t.Fatalf("工具结果应为 ABC,实际 %v", res) + } +} + +// 输出通道**同步等真实结果**:失败必须上报(§9.4 根治)。 +func TestPlugin_OutputChannelReportsRealFailure(t *testing.T) { + bin := buildTestPlugin(t, "stageplugin.go") + core := newFakeCore() + + host, err := NewHost() + if err != nil { + t.Fatalf("NewHost: %v", err) + } + defer host.Close() + + p := New("demo", bin, t.TempDir(), nil, host, nil) + if err := p.Start(core); err != nil { + t.Fatalf("Start: %v", err) + } + defer p.Close() + + core.mu.Lock() + h, ok := core.outputs["demo_ch"] + core.mu.Unlock() + if !ok { + t.Fatal("插件应注册 demo_ch 输出通道") + } + + // 成功路径 + res, err := h(map[string]interface{}{"payload": "hi", "type": "text"}) + if err != nil { + t.Fatalf("发送应成功: %v", err) + } + m, _ := res.(map[string]interface{}) + if m["status"] != "sent" { + t.Errorf("成功应返回 status=sent,实际 %v", m) + } + + // 失败路径:插件返回错误 → 调用方必须收到 error(而非假成功) + _, err = h(map[string]interface{}{"payload": "fail", "type": "text"}) + if err == nil { + t.Fatal("发送失败时必须上报 error(C ABI 路径此处永远假成功)") + } + if !strings.Contains(err.Error(), "缺少 user_id") { + t.Errorf("应透传插件的失败原因,实际: %v", err) + } +} + +// 插件在 plugin.start 期间反向调用内核(settings/autoRestart 等)。 +func TestPlugin_ReverseCallsDuringStart(t *testing.T) { + bin := buildTestPlugin(t, "stageplugin.go") + core := newFakeCore() + + host, err := NewHost() + if err != nil { + t.Fatalf("NewHost: %v", err) + } + defer host.Close() + + p := New("demo", bin, t.TempDir(), nil, host, nil) + if err := p.Start(core); err != nil { + t.Fatalf("Start: %v", err) + } + defer p.Close() + + if !core.autoStart { + t.Error("插件调用 lifecycle.autoRestart 后内核状态应更新") + } +} + +// 权限梯度显式化(§3.8):CoreSDK 不提供内核内部机制, +// 插件请求这些能力时必须被拒绝而非静默忽略。 +func TestCoreHandler_RejectsUnknownAndUnimplementedMethods(t *testing.T) { + h := &coreHandler{sdk: newFakeCore(), name: "x", locks: &lockRegistry{}} + + // 未知 method + if _, err := h.Handle("supervisor.restart", nil); err == nil { + t.Error("内核内部机制不应可达(应报未知 method)") + } + + // 事件订阅:今日 C ABI 是空实现(静默成功),这里必须明确报未实现 + if _, err := h.Handle(MethodEventsSubscribe, json.RawMessage(`{}`)); err == nil { + t.Error("事件订阅未落地时应明确报错,而非静默成功后收不到事件") + } + + // 多模态注入同理 + if _, err := h.Handle(MethodIOSetToolBlocks, json.RawMessage(`{}`)); err == nil { + t.Error("多模态注入未落地时应明确报错") + } +} + +// stage 锁在无进行中 stage 时申请应被拒绝(防止插件在 stage 外乱加锁)。 +func TestCoreHandler_StageLockOutsideStageRejected(t *testing.T) { + h := &coreHandler{sdk: newFakeCore(), name: "x", locks: &lockRegistry{}} + if _, err := h.Handle(MethodStageLock, nil); err == nil { + t.Error("stage 外加锁应被拒绝") + } + if !strings.Contains(fmt.Sprint(mustErr(h.Handle(MethodStageUnlock, nil))), "无进行中的 stage") { + t.Error("stage 外解锁的错误信息应说明原因") + } +} + +func mustErr(_ interface{}, err error) error { return err } + +// **跨进程 lost update 终极验证**:5 个独立插件进程并发读-改-写同一个 +// FinalText,全部标记必须保留。 +// +// 这是实验 8(5 进程 × 300 轮零丢失)在真实 RPC + 真实 RunStage 并发扇出 +// 下的复刻。对照今日 C ABI 副本模型实测 35.8~36.8% 丢失(§8.4)。 +func TestPlugin_FiveProcessesConcurrentAppendNoLostUpdate(t *testing.T) { + bin := buildTestPlugin(t, "appendplugin.go") + + // 关键:全部插件共享同一个 Host(同一 memfd) + host, err := NewHost() + if err != nil { + t.Fatalf("NewHost: %v", err) + } + defer host.Close() + + tags := []string{"A", "B", "C", "D", "E"} + var handlers []pubsdk.StageHandler + for _, tag := range tags { + core := newFakeCore() + p := New("append-"+tag, bin, t.TempDir(), nil, host, nil) + p.env = []string{"PLUGIN_TAG=" + tag} + if err := p.Start(core); err != nil { + t.Fatalf("插件 %s Start: %v", tag, err) + } + defer p.Close() + handlers = append(handlers, core.stageHandlers(pubsdk.StageAfterToolcall)...) + } + if len(handlers) != len(tags) { + t.Fatalf("应有 %d 个 handler,实际 %d", len(tags), len(handlers)) + } + + sc := &pubsdk.StageContext{ + Phase: pubsdk.StageAfterToolcall, + FinalText: "", + } + + if errs := runStageLikeKernel(handlers, sc); len(errs) > 0 { + t.Fatalf("并发 stage 执行失败: %v", errs) + } + + // 断言:各标记出现次数之和 == 最终长度 == 插件数 ⇒ 无丢失、无撕裂 + total := 0 + counts := map[string]int{} + for _, tag := range tags { + c := strings.Count(sc.FinalText, tag) + counts[tag] = c + total += c + } + if total != len(sc.FinalText) { + t.Fatalf("出现撕裂:各标记计数之和 %d != 最终长度 %d(final=%q counts=%v)", + total, len(sc.FinalText), sc.FinalText, counts) + } + if total != len(tags) { + t.Fatalf("出现 lost update:期望 %d 个插件的写入全部保留,实际 %d(final=%q counts=%v)", + len(tags), total, sc.FinalText, counts) + } + for tag, c := range counts { + if c != 1 { + t.Errorf("插件 %s 的写入丢失:期望 1 次,实际 %d 次", tag, c) + } + } +} diff --git a/internal/plugin/proc/stage.go b/internal/plugin/proc/stage.go new file mode 100644 index 0000000..ae7a702 --- /dev/null +++ b/internal/plugin/proc/stage.go @@ -0,0 +1,111 @@ +package proc + +import ( + "context" + "fmt" + "log" + "sync" + "time" + + pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk" +) + +// stage 执行:把内核的 RunStage 并发扇出接到共享段(§3.4,风险 3.4 的落点)。 +// +// 执行链路: +// +// 内核 RunStage(并发 go func,原始设计不变) +// └─ 外部插件 handler = coreHandler.runStage() +// ├─ Host.beginStage:首个到达者独占共享段并写入 StageContext +// ├─ stage.invoke RPC → 插件进程 +// │ └─ 插件侧:stage.lock → 读共享段 → handler → 只写脏字段 → stage.unlock +// └─ Host.endStage:最后离开者把共享段读回内核 StageContext + 压实 arena +// +// 关键性质: +// - **并发扇出保留**(§0.2 第 1 条:并发扇出是原始设计,不是缺陷) +// - **无副本**:全部插件 mmap 同一 memfd,在同一份状态上读改写,锁仲裁串行化临界区 +// - **只读插件零写入**:脏字段集为空 → 不可能覆盖他人改写 +// +// 对照今日 C ABI:每个插件拿到独立 JSON 副本,回传时无条件覆盖 10 个字段, +// 实测 35.8~36.8% lost update(§8.4),现网量级百分之几脏数据进 LLM(§8.6)。 + +// lockRegistry 持有当前进行中 stage 的锁,供插件的 stage.lock/unlock 路由。 +type lockRegistry struct { + mu sync.Mutex + lock *stageLock +} + +func (r *lockRegistry) bind(l *stageLock) { + r.mu.Lock() + r.lock = l + r.mu.Unlock() +} + +func (r *lockRegistry) current() *stageLock { + r.mu.Lock() + defer r.mu.Unlock() + return r.lock +} + +func (r *lockRegistry) acquire(plugin string) error { + l := r.current() + if l == nil { + return fmt.Errorf("stage.lock: 当前无进行中的 stage(插件 %s 在 stage 外加锁?)", plugin) + } + return l.Acquire(plugin) +} + +func (r *lockRegistry) release(plugin string) error { + l := r.current() + if l == nil { + return fmt.Errorf("stage.unlock: 当前无进行中的 stage(插件 %s)", plugin) + } + return l.Release(plugin) +} + +// forceRelease 在插件进程崩溃时释放其可能持有的锁(实验 9 的自愈机制)。 +func (r *lockRegistry) forceRelease(plugin string) bool { + l := r.current() + if l == nil { + return false + } + return l.ForceRelease(plugin) +} + +// stageInvokeTimeout 是单个插件执行 stage 的上限。 +// +// 取 30s:与内核工具超时(60s,toolcall.go)留出差距, +// 使 stage 超时能被识别为 stage 问题而非工具问题。 +// 超时后调用方返回错误,卡住的插件进程可由上层 Kill 回收—— +// **对比 cgo 路径超时后 OS 线程永久泄漏(现网 26 次,§9.3)**。 +const stageInvokeTimeout = 30 * time.Second + +// runStage 是注册到内核 StageHost 的 handler(每个外部插件一个)。 +func (h *coreHandler) runStage(stage string, sc *pubsdk.StageContext) error { + if h.host == nil { + return fmt.Errorf("插件 %s: stage %s 共享段未就绪", h.name, stage) + } + + coord, err := h.host.beginStage(sc) + if err != nil { + return fmt.Errorf("插件 %s stage %s: %w", h.name, stage, err) + } + defer func() { + if endErr := h.host.endStage(coord); endErr != nil { + log.Printf("[proc] %s stage %s 收尾失败: %v", h.name, stage, endErr) + } + }() + + ctx, cancel := context.WithTimeout(context.Background(), stageInvokeTimeout) + defer cancel() + + if err := h.invokeStageWithCtx(ctx, stage, coord.seg.Seq()); err != nil { + // 插件可能在持锁时失败(崩溃/超时)——强制释放,避免后续插件死锁。 + // 这正是"锁仲裁回内核"的自愈价值(实验 9):无需 robust mutex。 + if h.host.ForceReleaseLock(h.name) { + log.Printf("[proc] %s stage %s 失败后强制释放其持有的 stage 锁", h.name, stage) + } + return err + } + return nil +} diff --git a/internal/plugin/proc/testdata/appendplugin.go b/internal/plugin/proc/testdata/appendplugin.go new file mode 100644 index 0000000..ba37df4 --- /dev/null +++ b/internal/plugin/proc/testdata/appendplugin.go @@ -0,0 +1,232 @@ +//go:build ignore + +// appendplugin 在 stage 中把自己的标记追加到 FinalText(读-改-写)。 +// +// 用于跨进程 lost update 验证:多个此类插件并发处理同一 stage, +// 若全部标记都保留 ⇒ 无丢失;若少了 ⇒ 出现 lost update。 +// +// 这是实验 8(5 进程 × 300 轮零丢失)在真实 RPC + 真实内核 RunStage +// 下的复刻——机制单测已过,这里验证集成后同样成立。 +package main + +import ( + "bufio" + "encoding/binary" + "encoding/json" + "fmt" + "os" + "sync" + "syscall" +) + +type request struct { + ID uint64 `json:"id,omitempty"` + Method string `json:"method"` + Params json.RawMessage `json:"params,omitempty"` +} + +type response struct { + ID uint64 `json:"id"` + Result interface{} `json:"result,omitempty"` + Error string `json:"error,omitempty"` +} + +var ( + out = bufio.NewWriter(os.Stdout) + writeMu sync.Mutex + + nextID uint64 + pendMu sync.Mutex + pending = map[uint64]chan json.RawMessage{} + + shm []byte + tag string +) + +func send(v interface{}) { + b, _ := json.Marshal(v) + writeMu.Lock() + out.Write(b) + out.WriteByte('\n') + out.Flush() + writeMu.Unlock() +} + +func callKernel(method string, params interface{}) (json.RawMessage, bool) { + pendMu.Lock() + nextID++ + id := nextID + ch := make(chan json.RawMessage, 1) + pending[id] = ch + pendMu.Unlock() + + var raw json.RawMessage + if params != nil { + b, _ := json.Marshal(params) + raw = b + } + send(request{ID: id, Method: method, Params: raw}) + r, ok := <-ch + return r, ok +} + +const ( + headerSize = 64 + stageFieldCount = 18 + sliceSize = 8 + flagCount = 8 + + offArenaBase = 8 + offArenaCap = 12 + offArenaUsed = 16 + offCtxBase = 20 + offSeq = 24 + + fFinalText = 5 // 与 proc/shmcodec.go 的 stageField 枚举顺序一致 +) + +func desc(field int) (uint32, uint32) { + cb := binary.LittleEndian.Uint32(shm[offCtxBase:]) + o := cb + uint32(field*sliceSize) + return binary.LittleEndian.Uint32(shm[o:]), binary.LittleEndian.Uint32(shm[o+4:]) +} + +func setDesc(field int, off, ln uint32) { + cb := binary.LittleEndian.Uint32(shm[offCtxBase:]) + o := cb + uint32(field*sliceSize) + binary.LittleEndian.PutUint32(shm[o:], off) + binary.LittleEndian.PutUint32(shm[o+4:], ln) +} + +func readFinalText() string { + off, ln := desc(fFinalText) + if off == 0 && ln == 0 { + return "" + } + if ln == 0 { + return "" + } + base := binary.LittleEndian.Uint32(shm[offArenaBase:]) + return string(shm[base+off : base+off+ln]) +} + +func writeFinalText(s string) error { + used := binary.LittleEndian.Uint32(shm[offArenaUsed:]) + if used == 0 { + used = 1 + } + cap_ := binary.LittleEndian.Uint32(shm[offArenaCap:]) + if used+uint32(len(s)) > cap_ { + return fmt.Errorf("arena 空间不足") + } + base := binary.LittleEndian.Uint32(shm[offArenaBase:]) + copy(shm[base+used:], []byte(s)) + binary.LittleEndian.PutUint32(shm[offArenaUsed:], used+uint32(len(s))) + setDesc(fFinalText, used, uint32(len(s))) + // 世代号自增 + v := binary.LittleEndian.Uint64(shm[offSeq:]) + binary.LittleEndian.PutUint64(shm[offSeq:], v+1) + return nil +} + +func main() { + tag = os.Getenv("PLUGIN_TAG") + if tag == "" { + tag = "?" + } + + in := bufio.NewScanner(bufio.NewReader(os.Stdin)) + in.Buffer(make([]byte, 0, 64*1024), 1024*1024) + + for in.Scan() { + line := make([]byte, len(in.Bytes())) + copy(line, in.Bytes()) + + var probe struct { + ID uint64 `json:"id"` + Method string `json:"method"` + } + if json.Unmarshal(line, &probe) != nil { + continue + } + if probe.Method == "" { + var resp struct { + ID uint64 `json:"id"` + Result json.RawMessage `json:"result"` + } + json.Unmarshal(line, &resp) + pendMu.Lock() + ch, ok := pending[resp.ID] + delete(pending, resp.ID) + pendMu.Unlock() + if ok { + ch <- resp.Result + } + continue + } + + var req request + json.Unmarshal(line, &req) + + switch req.Method { + case "handshake": + var hp struct { + ShmSize int `json:"shm_size"` + } + json.Unmarshal(req.Params, &hp) + if hp.ShmSize > 0 { + m, err := syscall.Mmap(3, 0, hp.ShmSize, + syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED) + if err != nil { + send(response{ID: req.ID, Error: fmt.Sprintf("mmap: %v", err)}) + continue + } + shm = m + } + send(response{ID: req.ID, Result: map[string]interface{}{ + "protocol": 1, "sdk_version": "test", + "plugin_name": "append-" + tag, "pid": os.Getpid(), + }}) + + case "plugin.init": + send(response{ID: req.ID}) + + case "plugin.start": + go func(id uint64) { + callKernel("stage.register", map[string]interface{}{ + "stage": "after_toolcall", + "scope": "global", + }) + send(response{ID: id}) + }(req.ID) + + case "plugin.stop": + send(response{ID: req.ID}) + out.Flush() + os.Exit(0) + + case "stage.invoke": + go func(id uint64) { + if shm == nil { + send(response{ID: id, Error: "共享段未挂载"}) + return + } + // 拿锁 → 读 → 追加自己的标记 → 写回 → 放锁 + callKernel("stage.lock", nil) + cur := readFinalText() + err := writeFinalText(cur + tag) + callKernel("stage.unlock", nil) + if err != nil { + send(response{ID: id, Error: err.Error()}) + return + } + send(response{ID: id, Result: map[string]interface{}{"dirty_fields": 1}}) + }(req.ID) + + default: + if req.ID != 0 { + send(response{ID: req.ID}) + } + } + } +} diff --git a/internal/plugin/proc/testdata/readonlyplugin.go b/internal/plugin/proc/testdata/readonlyplugin.go new file mode 100644 index 0000000..74a3fe8 --- /dev/null +++ b/internal/plugin/proc/testdata/readonlyplugin.go @@ -0,0 +1,178 @@ +//go:build ignore + +// readonlyplugin 是只读 stage 插件(模拟 weather 的 AfterToolcall): +// 读取共享段但不写回任何字段。 +// +// **这是 lost update 修复的关键验证对象**:C ABI 副本模型下, +// 它会把自己收到的旧快照无条件回传,覆盖 sanitizer 的清洗结果 +// (§8.6 实测现网 1.6~4.3% 被覆盖)。共享内存模型下它零写入。 +package main + +import ( + "bufio" + "encoding/binary" + "encoding/json" + "fmt" + "os" + "sync" + "syscall" +) + +type request struct { + ID uint64 `json:"id,omitempty"` + Method string `json:"method"` + Params json.RawMessage `json:"params,omitempty"` +} + +type response struct { + ID uint64 `json:"id"` + Result interface{} `json:"result,omitempty"` + Error string `json:"error,omitempty"` +} + +var ( + out = bufio.NewWriter(os.Stdout) + writeMu sync.Mutex + + nextID uint64 + pendMu sync.Mutex + pending = map[uint64]chan json.RawMessage{} + + shm []byte +) + +func send(v interface{}) { + b, _ := json.Marshal(v) + writeMu.Lock() + out.Write(b) + out.WriteByte('\n') + out.Flush() + writeMu.Unlock() +} + +func callKernel(method string, params interface{}) json.RawMessage { + pendMu.Lock() + nextID++ + id := nextID + ch := make(chan json.RawMessage, 1) + pending[id] = ch + pendMu.Unlock() + + var raw json.RawMessage + if params != nil { + b, _ := json.Marshal(params) + raw = b + } + send(request{ID: id, Method: method, Params: raw}) + return <-ch +} + +const ( + offArenaBase = 8 + offCtxBase = 20 + sliceSize = 8 + fToolResults = 10 +) + +func readToolResults() []byte { + base := binary.LittleEndian.Uint32(shm[offArenaBase:]) + cb := binary.LittleEndian.Uint32(shm[offCtxBase:]) + o := cb + uint32(fToolResults*sliceSize) + off := binary.LittleEndian.Uint32(shm[o:]) + ln := binary.LittleEndian.Uint32(shm[o+4:]) + if off == 0 && ln == 0 { + return nil + } + return shm[base+off : base+off+ln] +} + +func main() { + in := bufio.NewScanner(bufio.NewReader(os.Stdin)) + in.Buffer(make([]byte, 0, 64*1024), 1024*1024) + + for in.Scan() { + line := make([]byte, len(in.Bytes())) + copy(line, in.Bytes()) + + var probe struct { + ID uint64 `json:"id"` + Method string `json:"method"` + } + if json.Unmarshal(line, &probe) != nil { + continue + } + if probe.Method == "" { + var resp struct { + ID uint64 `json:"id"` + Result json.RawMessage `json:"result"` + } + json.Unmarshal(line, &resp) + pendMu.Lock() + ch, ok := pending[resp.ID] + delete(pending, resp.ID) + pendMu.Unlock() + if ok { + ch <- resp.Result + } + continue + } + + var req request + json.Unmarshal(line, &req) + + switch req.Method { + case "handshake": + var hp struct { + ShmSize int `json:"shm_size"` + } + json.Unmarshal(req.Params, &hp) + if hp.ShmSize > 0 { + m, err := syscall.Mmap(3, 0, hp.ShmSize, + syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED) + if err != nil { + send(response{ID: req.ID, Error: fmt.Sprintf("mmap: %v", err)}) + continue + } + shm = m + } + send(response{ID: req.ID, Result: map[string]interface{}{ + "protocol": 1, "sdk_version": "test", "plugin_name": "readonly", "pid": os.Getpid(), + }}) + + case "plugin.init": + send(response{ID: req.ID}) + + case "plugin.start": + go func(id uint64) { + callKernel("stage.register", map[string]interface{}{ + "stage": "after_toolcall", + "scope": "global", + }) + send(response{ID: id}) + }(req.ID) + + case "plugin.stop": + send(response{ID: req.ID}) + out.Flush() + os.Exit(0) + + case "stage.invoke": + go func(id uint64) { + if shm == nil { + send(response{ID: id, Error: "共享段未挂载"}) + return + } + callKernel("stage.lock", nil) + // 只读:读了但一个字节都不写回 + _ = readToolResults() + callKernel("stage.unlock", nil) + send(response{ID: id, Result: map[string]interface{}{"dirty_fields": 0}}) + }(req.ID) + + default: + if req.ID != 0 { + send(response{ID: req.ID}) + } + } + } +} diff --git a/internal/plugin/proc/testdata/stageplugin.go b/internal/plugin/proc/testdata/stageplugin.go new file mode 100644 index 0000000..91baf3c --- /dev/null +++ b/internal/plugin/proc/testdata/stageplugin.go @@ -0,0 +1,311 @@ +//go:build ignore + +// stageplugin 是完整形态的测试插件:注册工具/阶段/输出通道, +// stage 处理经共享内存读改写(模拟 sanitizer 的清洗行为)。 +// +// 它手写 RPC 与共享段访问,不依赖公开 SDK——因为 SDK 侧的 proc 支持 +// 属于 Part 3(plugindev 工具链)的内容。这里只验证内核侧机制。 +package main + +import ( + "bufio" + "encoding/binary" + "encoding/json" + "fmt" + "os" + "strings" + "sync" + "syscall" +) + +type request struct { + ID uint64 `json:"id,omitempty"` + Method string `json:"method"` + Params json.RawMessage `json:"params,omitempty"` +} + +type response struct { + ID uint64 `json:"id"` + Result interface{} `json:"result,omitempty"` + Error string `json:"error,omitempty"` +} + +var ( + out = bufio.NewWriter(os.Stdout) + writeMu sync.Mutex + + nextID uint64 + pendMu sync.Mutex + pending = map[uint64]chan json.RawMessage{} + + shm []byte + shmSize int +) + +func send(v interface{}) { + b, _ := json.Marshal(v) + writeMu.Lock() + out.Write(b) + out.WriteByte('\n') + out.Flush() + writeMu.Unlock() +} + +func callKernel(method string, params interface{}) json.RawMessage { + pendMu.Lock() + nextID++ + id := nextID + ch := make(chan json.RawMessage, 1) + pending[id] = ch + pendMu.Unlock() + + var raw json.RawMessage + if params != nil { + b, _ := json.Marshal(params) + raw = b + } + send(request{ID: id, Method: method, Params: raw}) + return <-ch +} + +// ---- 共享段访问(与内核 proc 包的布局一致)---- + +const ( + headerSize = 64 + // 与 proc 包保持一致:18 个字段 × 8 字节 + 8 字节标志位 + stageFieldCount = 18 + sliceSize = 8 + flagCount = 8 + ctxSize = stageFieldCount*sliceSize + flagCount + + offArenaBase = 8 + offArenaCap = 12 + offArenaUsed = 16 + offCtxBase = 20 + offSeq = 24 + + // 字段索引(与 proc/shmcodec.go 的 stageField 枚举顺序一致) + fToolResults = 10 +) + +func arenaBase() uint32 { return binary.LittleEndian.Uint32(shm[offArenaBase:]) } +func arenaCap() uint32 { return binary.LittleEndian.Uint32(shm[offArenaCap:]) } +func ctxBase() uint32 { return binary.LittleEndian.Uint32(shm[offCtxBase:]) } + +func descOffset(field int) uint32 { return ctxBase() + uint32(field*sliceSize) } + +func getDesc(field int) (off, ln uint32) { + o := descOffset(field) + return binary.LittleEndian.Uint32(shm[o:]), binary.LittleEndian.Uint32(shm[o+4:]) +} + +func setDesc(field int, off, ln uint32) { + o := descOffset(field) + binary.LittleEndian.PutUint32(shm[o:], off) + binary.LittleEndian.PutUint32(shm[o+4:], ln) +} + +func readField(field int) []byte { + off, ln := getDesc(field) + if off == 0 && ln == 0 { + return nil + } + if ln == 0 { + return []byte{} + } + base := arenaBase() + return shm[base+off : base+off+ln] +} + +func writeField(field int, data []byte) error { + used := binary.LittleEndian.Uint32(shm[offArenaUsed:]) + if used == 0 { + used = 1 + } + end := used + uint32(len(data)) + if end > arenaCap() { + return fmt.Errorf("arena 空间不足") + } + base := arenaBase() + copy(shm[base+used:], data) + binary.LittleEndian.PutUint32(shm[offArenaUsed:], end) + setDesc(field, used, uint32(len(data))) + return nil +} + +func bumpSeq() { + v := binary.LittleEndian.Uint64(shm[offSeq:]) + binary.LittleEndian.PutUint64(shm[offSeq:], v+1) +} + +type toolResult struct { + CallID string `json:"call_id"` + Name string `json:"name"` + Plugin string `json:"plugin,omitempty"` + Success bool `json:"success"` + Result interface{} `json:"result"` +} + +// handleStage 模拟 sanitizer:拿锁 → 读 ToolResults → 剥 ANSI → 只写脏字段 → 放锁 +func handleStage() (int, error) { + callKernel("stage.lock", nil) + defer callKernel("stage.unlock", nil) + + raw := readField(fToolResults) + if len(raw) == 0 { + return 0, nil + } + var results []toolResult + if err := json.Unmarshal(raw, &results); err != nil { + return 0, err + } + + before := string(raw) + for i := range results { + if s, ok := results[i].Result.(string); ok { + results[i].Result = strings.NewReplacer("\x1b[31m", "", "\x1b[0m", "").Replace(s) + } + } + after, _ := json.Marshal(results) + + // 只有真变了才写回 —— 这是消除 lost update 的核心 + if string(after) == before { + return 0, nil + } + if err := writeField(fToolResults, after); err != nil { + return 0, err + } + bumpSeq() + return 1, nil +} + +func main() { + in := bufio.NewScanner(bufio.NewReader(os.Stdin)) + in.Buffer(make([]byte, 0, 64*1024), 1024*1024) + + for in.Scan() { + line := make([]byte, len(in.Bytes())) + copy(line, in.Bytes()) + + var probe struct { + ID uint64 `json:"id"` + Method string `json:"method"` + } + if json.Unmarshal(line, &probe) != nil { + continue + } + + // 内核对我们反向调用的应答 + if probe.Method == "" { + var resp struct { + ID uint64 `json:"id"` + Result json.RawMessage `json:"result"` + } + json.Unmarshal(line, &resp) + pendMu.Lock() + ch, ok := pending[resp.ID] + delete(pending, resp.ID) + pendMu.Unlock() + if ok { + ch <- resp.Result + } + continue + } + + var req request + json.Unmarshal(line, &req) + + switch req.Method { + case "handshake": + var hp struct { + ShmSize int `json:"shm_size"` + } + json.Unmarshal(req.Params, &hp) + shmSize = hp.ShmSize + if shmSize > 0 { + // fd 3 = 内核传入的共享段 memfd + m, err := syscall.Mmap(3, 0, shmSize, + syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED) + if err != nil { + send(response{ID: req.ID, Error: fmt.Sprintf("mmap 共享段失败: %v", err)}) + continue + } + shm = m + } + send(response{ID: req.ID, Result: map[string]interface{}{ + "protocol": 1, "sdk_version": "test", "plugin_name": "stage", "pid": os.Getpid(), + }}) + + case "plugin.init": + send(response{ID: req.ID}) + + case "plugin.start": + go func(id uint64) { + callKernel("lifecycle.autoRestart", map[string]interface{}{"enabled": true}) + callKernel("tool.register", map[string]interface{}{ + "name": "demo_upper", + "def": map[string]interface{}{"name": "demo_upper", "description": "转大写"}, + }) + callKernel("stage.register", map[string]interface{}{ + "stage": "after_toolcall", + "scope": "global", + }) + callKernel("output.register", map[string]interface{}{ + "name": "demo_ch", + "caps": 1, + "desc": "测试通道", + }) + send(response{ID: id}) + }(req.ID) + + case "plugin.stop": + send(response{ID: req.ID}) + out.Flush() + os.Exit(0) + + case "tool.invoke": + var p struct { + Name string `json:"name"` + Args map[string]interface{} `json:"args"` + } + json.Unmarshal(req.Params, &p) + text, _ := p.Args["text"].(string) + send(response{ID: req.ID, Result: map[string]interface{}{ + "result": strings.ToUpper(text), + }}) + + case "stage.invoke": + go func(id uint64) { + if shm == nil { + send(response{ID: id, Error: "共享段未挂载"}) + return + } + n, err := handleStage() + if err != nil { + send(response{ID: id, Error: err.Error()}) + return + } + send(response{ID: id, Result: map[string]interface{}{"dirty_fields": n}}) + }(req.ID) + + case "output.invoke": + var p struct { + Channel string `json:"channel"` + Args map[string]interface{} `json:"args"` + } + json.Unmarshal(req.Params, &p) + payload, _ := p.Args["payload"].(string) + if payload == "fail" { + // 模拟现网 qq 插件的真实失败:meta 缺 user_id + send(response{ID: req.ID, Error: "meta 中缺少 user_id 字段"}) + continue + } + send(response{ID: req.ID, Result: map[string]interface{}{"status": "sent"}}) + + default: + if req.ID != 0 { + send(response{ID: req.ID}) + } + } + } +}