package app import ( "context" "encoding/json" "errors" "fmt" "log" "os" "path/filepath" agentcommand "agent-command" "agent-system-backend/internal/cmdbus" "agent-system-backend/internal/server" "agent-system-backend/pkg/extension" core "agent-system-core" ) type compiledRegistry struct { skillsRoot string bus *cmdbus.Bus } // pluginRegistry 携带插件身份与宿主能力,per-InProc 构造: // AddCommandHandler 绑定的处理器在分发时按请求 Owner 构造 inprocHost(EXT-1)。 type pluginRegistry struct { compiledRegistry pluginID string caps hostcaps // summary 是本插件的注册面摘要(WP3-I4):AddAdminZone 声明记录于此, // server 经 CompiledPlugins 供给分区清单端点(WP3-I3)。 summary *server.CompiledPlugin } // AddAdminZone 记录管理区分区声明(WP3-I3):声明即展示元数据,权威记录 // 就是注册期摘要;规格非法(非对象、id/title 缺失)fail-closed 拒绝, // 使 register() 整体中止(与命令注册同一失败语义)。 func (r pluginRegistry) AddAdminZone(spec json.RawMessage) error { if r.summary == nil { return errors.New("extension: " + r.pluginID + ": admin zone outside applyCompiled") } var s extension.AdminZoneSpec if err := json.Unmarshal(spec, &s); err != nil { return fmt.Errorf("extension: admin zone spec: %w", err) } if s.ID == "" || s.Title == "" { return fmt.Errorf("extension: %s: admin zone requires id and title", r.pluginID) } r.summary.AdminZones = append(r.summary.AdminZones, server.AdminZoneSection{ ID: s.ID, Title: s.Title, Icon: s.Icon, Desc: s.Desc, Order: s.Order, }) return nil } func (r pluginRegistry) AddCommandHandler(spec json.RawMessage, h extension.Handler) error { if r.bus == nil { return nil } if h == nil { return fmt.Errorf("extension: %s: nil command handler", r.pluginID) } var s agentcommand.CommandSpec if err := json.Unmarshal(spec, &s); err != nil { return fmt.Errorf("extension: command spec: %w", err) } return r.bus.RegisterPluginCommand(s, func(ctx context.Context, req cmdbus.Request) (any, error) { host := inprocHost{caps: r.caps, pluginID: r.pluginID, owner: req.Owner, actor: req.Actor} return h(ctx, host, extension.Request{ ID: req.ID, Params: req.Params, Entrance: req.Entrance, Actor: req.Actor, Owner: req.Owner, ConversationID: req.ConversationID, HostProjectID: req.HostProjectID, }) }) } // AddIntent 意图声明(G2-2):汇入总线意图表;插件身份随声明归因。 func (r pluginRegistry) AddIntent(spec json.RawMessage) error { if r.bus == nil { return nil } var is extension.IntentSpec if err := json.Unmarshal(spec, &is); err != nil { return fmt.Errorf("extension: intent spec: %w", err) } return r.bus.Intents().Declare(cmdbus.Intent{ ID: is.ID, Phrases: is.Phrases, Command: is.Command, Params: is.Params, Description: is.Description, Plugin: r.pluginID, }) } func (r compiledRegistry) AddSkill(name, markdown string) error { if r.skillsRoot == "" || name == "" { return nil } dest := filepath.Join(r.skillsRoot, "compiled", name) if err := os.MkdirAll(dest, 0o755); err != nil { return err } return os.WriteFile(filepath.Join(dest, "SKILL.md"), []byte(markdown), 0o644) } func (r compiledRegistry) AddCommand(spec json.RawMessage) error { if r.bus == nil { return nil } return r.bus.RegisterPluginCommands([]json.RawMessage{spec}) } // RegisterResourceAuthorizer 声明式扩展无宿主进程内代码,不能提供回调(fail-closed)。 func (r compiledRegistry) RegisterResourceAuthorizer(extension.ResourceAuthorizer) error { return errors.New("extension: declarative plugins cannot register resource authorizers") } // RegisterResourceAuthorizer 进程内扩展(EXT-1):填充 EXT-8 适配器槽 // (hostcaps.authz 共享指针;首个注册者生效,组合根已装配进 Server/runmgr)。 // 此前 pluginRegistry 未覆盖该方法——进程内插件注册适配器会命中声明式分支的 // 拒绝,register() 整体中止,插件命令全部未注册(RDM 二进制 rdm.* 全部 // 「unknown command」;MS1 重验收烟测实测发现)。 func (r pluginRegistry) RegisterResourceAuthorizer(fn extension.ResourceAuthorizer) error { if fn == nil { return errors.New("extension: " + r.pluginID + ": nil resource authorizer") } r.caps.authz.set(r.pluginID, fn) return nil } func (r compiledRegistry) Note(kind, name string, payload json.RawMessage) error { log.Printf("extension: note %s %s (%d bytes)", kind, name, len(payload)) return nil } // recordRegistry 包装 per-plugin 注册表,把成功注册的声明资产名记入摘要 // (WP3-I4:注册面即清单面——编译插件的管理条目不另设声明口)。 type recordRegistry struct { extension.Registry sum *server.CompiledPlugin } func (r recordRegistry) AddCommand(spec json.RawMessage) error { if err := r.Registry.AddCommand(spec); err != nil { return err } if id := specID(spec); id != "" { r.sum.Commands = append(r.sum.Commands, id) } return nil } func (r recordRegistry) AddCommandHandler(spec json.RawMessage, h extension.Handler) error { if err := r.Registry.AddCommandHandler(spec, h); err != nil { return err } if id := specID(spec); id != "" { r.sum.Commands = append(r.sum.Commands, id) } return nil } func (r recordRegistry) AddIntent(spec json.RawMessage) error { if err := r.Registry.AddIntent(spec); err != nil { return err } if id := specID(spec); id != "" { r.sum.Intents = append(r.sum.Intents, id) } return nil } func (r recordRegistry) AddSkill(name, markdown string) error { if err := r.Registry.AddSkill(name, markdown); err != nil { return err } r.sum.Skills = append(r.sum.Skills, name) return nil } // specID 宽松提取 spec 顶层 id:仅服务清单摘要展示,解析失败返回空串 // 不记录,不影响注册结果本身。 func specID(spec json.RawMessage) string { var s struct { ID string `json:"id"` } if err := json.Unmarshal(spec, &s); err != nil { return "" } return s.ID } func applyCompiled(skillsRoot string, bus *cmdbus.Bus, caps hostcaps) []server.CompiledPlugin { base := compiledRegistry{skillsRoot: skillsRoot, bus: bus} var out []server.CompiledPlugin for _, e := range extension.Compiled() { if e.Register == nil { continue } sum := server.CompiledPlugin{ID: e.ID} reg := pluginRegistry{compiledRegistry: base, pluginID: e.ID, caps: caps, summary: &sum} // Register 失败的插件保留已成功注册部分(与命令总线一致:部分注册 // 生效),摘要如实反映实际注册面。 if err := e.Register(recordRegistry{Registry: reg, sum: &sum}); err != nil { log.Printf("extension: %s: %v", e.ID, err) } out = append(out, sum) } return out } func mergeMCPServers(raw string, extra []core.PluginMCPServer) string { var base []json.RawMessage if raw != "" { if err := json.Unmarshal([]byte(raw), &base); err != nil { return raw } } for _, s := range extra { b, err := json.Marshal(s) if err != nil { continue } base = append(base, b) } if len(base) == 0 { return "" } out, err := json.Marshal(base) if err != nil { return raw } return string(out) }