plugin disable system: kernel→SDK PluginManager + WebUI/CLI

- New disabled_plugins table (name, disabled_at, disabled_by)
- SDK.PluginManager interface: DisablePlugin/EnablePlugin/ListDisabledPlugins
- Registry implements PluginManager, wired into PluginSDK
- WebUI: POST /api/v1/plugins/<name>/disable|enable + plugins page with toggle
- Disabling webui shows confirmation dialog
- CLI: /plugin disable <name> / /plugin enable <name>
- pluginmgr removePlugin sync-cleanup from disabled_plugins table
This commit is contained in:
root
2026-07-29 15:07:32 +08:00
parent a899d777c3
commit 796a48dae5
8 changed files with 339 additions and 39 deletions

View File

@ -53,6 +53,58 @@ func (r *ConfigRegistry) initCoreTable() {
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)`)
r.db.Exec(`CREATE TABLE IF NOT EXISTS disabled_plugins (
name TEXT PRIMARY KEY,
disabled_at TEXT NOT NULL,
disabled_by TEXT NOT NULL DEFAULT ''
)`)
}
func (r *ConfigRegistry) AddDisabledPlugin(name, by string) error {
r.mu.Lock()
defer r.mu.Unlock()
_, err := r.db.Exec(`INSERT OR REPLACE INTO disabled_plugins (name, disabled_at, disabled_by) VALUES (?, datetime('now'), ?)`, name, by)
return err
}
func (r *ConfigRegistry) RemoveDisabledPlugin(name string) error {
r.mu.Lock()
defer r.mu.Unlock()
_, err := r.db.Exec(`DELETE FROM disabled_plugins WHERE name = ?`, name)
return err
}
type DisabledPluginInfo struct {
Name string `json:"name"`
DisabledAt string `json:"disabled_at"`
DisabledBy string `json:"disabled_by"`
}
func (r *ConfigRegistry) ListDisabledPlugins() ([]DisabledPluginInfo, error) {
r.mu.RLock()
defer r.mu.RUnlock()
rows, err := r.db.Query(`SELECT name, disabled_at, disabled_by FROM disabled_plugins ORDER BY name`)
if err != nil {
return nil, err
}
defer rows.Close()
var list []DisabledPluginInfo
for rows.Next() {
var info DisabledPluginInfo
if err := rows.Scan(&info.Name, &info.DisabledAt, &info.DisabledBy); err != nil {
return nil, err
}
list = append(list, info)
}
return list, rows.Err()
}
func (r *ConfigRegistry) IsPluginDisabled(name string) bool {
r.mu.RLock()
defer r.mu.RUnlock()
var count int
r.db.QueryRow(`SELECT COUNT(*) FROM disabled_plugins WHERE name = ?`, name).Scan(&count)
return count > 0
}
func (r *ConfigRegistry) ensurePluginTable(name string) {

View File

@ -22,7 +22,7 @@ import (
type NativeFactory func(name string, config map[string]interface{}) (sdk.Plugin, error)
// PluginMeta 插件显示名称元数据。
// PluginMeta 插件显示名称元数据,来源于 plg.json / RegisterPluginMeta
type PluginMeta struct {
NameZh string `json:"name_zh"`
NameEn string `json:"name_en"`
@ -189,6 +189,7 @@ func (r *Registry) buildSDK(name string) *sdk.PluginSDK {
RegAPI: regAPI,
RegOutput: regOutput,
RegInput: regInput,
PluginMgr: r,
})
}
@ -281,11 +282,7 @@ func (r *Registry) isDisabled(name string) bool {
if r.cfgReg == nil {
return false
}
v, err := r.cfgReg.PluginConfig(name).Get("disabled")
if err != nil || v == nil {
return false
}
return fmt.Sprint(v) == "true"
return r.cfgReg.IsPluginDisabled(name)
}
func (r *Registry) loadOne(plgDir, name string) bool {
@ -437,7 +434,7 @@ func (r *Registry) IsDisabled(name string) bool {
func (r *Registry) Enable(name string) error {
if r.cfgReg != nil {
r.cfgReg.PluginConfig(name).Set("disabled", "false")
r.cfgReg.RemoveDisabledPlugin(name)
}
r.mu.Lock()
delete(r.knownDisabled, name)
@ -473,12 +470,74 @@ func (r *Registry) Disable(name string) error {
}
if r.cfgReg != nil {
r.cfgReg.PluginConfig(name).Set("disabled", "true")
r.cfgReg.AddDisabledPlugin(name, "system")
}
log.Printf("[plugin] disabled: %s", name)
return nil
}
// ---- PluginManager interface ----
func (r *Registry) ListLoadedPlugins() []string { return r.List() }
func (r *Registry) ListDisabledPlugins() []sdk.DisabledPluginInfo {
if r.cfgReg == nil {
return nil
}
list, err := r.cfgReg.ListDisabledPlugins()
if err != nil {
return nil
}
result := make([]sdk.DisabledPluginInfo, len(list))
for i, v := range list {
result[i] = sdk.DisabledPluginInfo(v)
}
return result
}
func (r *Registry) IsPluginDisabled(name string) bool { return r.isDisabled(name) }
func (r *Registry) DisablePlugin(name, by string) error {
// Check not disabling self if running
if r.cfgReg != nil {
// If already disabled, no-op
if r.cfgReg.IsPluginDisabled(name) {
return fmt.Errorf("plugin %s already disabled", name)
}
}
r.mu.Lock()
p, ok := r.plugins[name]
if ok {
if err := p.Stop(); err != nil {
log.Printf("[plugin] stop %s for disable: %v", name, err)
}
delete(r.plugins, name)
for i, inst := range r.instances {
if inst.Name() == name {
r.instances = append(r.instances[:i], r.instances[i+1:]...)
break
}
}
}
r.knownDisabled[name] = true
r.mu.Unlock()
if r.toolCleaner != nil {
r.toolCleaner.UnregisterPluginTools(name)
}
if r.cfgReg != nil {
r.cfgReg.AddDisabledPlugin(name, by)
}
log.Printf("[plugin] disabled: %s (by %s)", name, by)
return nil
}
func (r *Registry) EnablePlugin(name string) error { return r.Enable(name) }
func (r *Registry) ReloadPlugins() (string, error) { return r.Reload(r.plgDir) }
// ListKnown 返回所有已知插件(已加载 + 已禁用 + 已安装但未加载)。
func (r *Registry) ListKnown() []string {
r.mu.RLock()

View File

@ -221,7 +221,7 @@ func (p *Plugin) handleBuiltin(conn net.Conn, line string, s *sdk.PluginSDK) boo
case "/settings":
p.cmdSettings(conn, parts)
case "/plugin":
p.cmdPlugin(conn, parts)
p.cmdPlugin(conn, parts, s)
case "/memory":
p.cmdMemory(conn, parts, s)
case "/knowledge":
@ -244,9 +244,11 @@ func (p *Plugin) cmdHelp(conn net.Conn) {
/settings 列出所有配置
/settings set <key> <val> 修改配置项
/settings core.llm 按前缀筛选
/plugin list 列出已安装插件
/plugin list 列出所有插件
/plugin install <url> 安装插件(需回环网络)
/plugin remove <name> 卸载插件
/plugin disable <name> 禁用插件
/plugin enable <name> 启用插件
/plugin info <name> 查看插件详情
/memory query <关键词> 查询图记忆
/knowledge 列出知识库
@ -347,26 +349,49 @@ func (p *Plugin) cmdSettings(conn net.Conn, parts []string) {
// ======== /plugin ========
func (p *Plugin) cmdPlugin(conn net.Conn, parts []string) {
func (p *Plugin) cmdPlugin(conn net.Conn, parts []string, s *sdk.PluginSDK) {
if len(parts) < 2 {
writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /plugin list|install <url>|remove <name>|info <name>"})
writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /plugin list|install <url>|remove <name>|info <name>|disable <name>|enable <name>"})
return
}
pmgr := s.PluginMgr()
switch parts[1] {
case "list":
if pluginReg == nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin registry not available"})
if pmgr == nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin manager not available"})
return
}
names := pluginReg.List()
if len(names) == 0 {
writeLine(conn, map[string]interface{}{"type": "response", "content": "无已加载插件"})
names := pmgr.ListLoadedPlugins()
disabled := pmgr.ListDisabledPlugins()
disabledNames := make(map[string]bool)
for _, d := range disabled {
disabledNames[d.Name] = true
}
all := make(map[string]string)
for _, n := range names {
all[n] = "\033[32m已加载\033[0m"
}
for _, d := range disabled {
if _, ok := all[d.Name]; !ok {
all[d.Name] = "\033[31m已禁用\033[0m"
} else {
all[d.Name] = "\033[32m已加载\033[0m (禁用将在重启后生效)"
}
}
if len(all) == 0 {
writeLine(conn, map[string]interface{}{"type": "response", "content": "无插件"})
return
}
var lines []string
for name, status := range all {
lines = append(lines, fmt.Sprintf(" %s %s", name, status))
}
sort.Strings(lines)
writeLine(conn, map[string]interface{}{
"type": "response",
"content": fmt.Sprintf("已加载插件 (%d):\n %s", len(names), strings.Join(names, "\n ")),
"content": fmt.Sprintf("插件 (%d):\n%s", len(all), strings.Join(lines, "\n")),
})
case "install":
@ -394,6 +419,10 @@ func (p *Plugin) cmdPlugin(conn net.Conn, parts []string) {
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
return
}
// 同步清理禁用表
if pmgr != nil {
pmgr.EnablePlugin(name)
}
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %s 已删除,执行 /plugin reload 生效", name)})
case "info":
@ -407,24 +436,58 @@ func (p *Plugin) cmdPlugin(conn net.Conn, parts []string) {
}
plg := pluginReg.Get(parts[2])
if plg == nil {
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %q 未加载", parts[2])})
if pmgr != nil && pmgr.IsPluginDisabled(parts[2]) {
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %q 已禁用", parts[2])})
return
}
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %q 未安装", parts[2])})
return
}
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("名称: %s\n状态: 已加载", plg.Name())})
case "reload":
if pluginReg == nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin registry not available"})
case "disable":
if len(parts) < 3 {
writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /plugin disable <name>"})
return
}
if _, err := pluginReg.Reload(pluginDir); err != nil {
if pmgr == nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin manager not available"})
return
}
if err := pmgr.DisablePlugin(parts[2], "cli"); err != nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
return
}
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %s 已禁用", parts[2])})
case "enable":
if len(parts) < 3 {
writeLine(conn, map[string]interface{}{"type": "response", "content": "用法: /plugin enable <name>"})
return
}
if pmgr == nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin manager not available"})
return
}
if err := pmgr.EnablePlugin(parts[2]); err != nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
return
}
writeLine(conn, map[string]interface{}{"type": "response", "content": fmt.Sprintf("插件 %s 已启用", parts[2])})
case "reload":
if pmgr == nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": "plugin manager not available"})
return
}
if _, err := pmgr.ReloadPlugins(); err != nil {
writeLine(conn, map[string]interface{}{"type": "error", "error": err.Error()})
return
}
writeLine(conn, map[string]interface{}{"type": "response", "content": "插件已重载"})
default:
writeLine(conn, map[string]interface{}{"type": "response", "content": "未知: /plugin " + parts[1] + "。支持: list, install, remove, info, reload"})
writeLine(conn, map[string]interface{}{"type": "response", "content": "未知: /plugin " + parts[1] + "。支持: list, install, remove, info, disable, enable, reload"})
}
}

View File

@ -455,6 +455,11 @@ func (p *Plugin) removePlugin(name string) (interface{}, error) {
return map[string]interface{}{"error": err.Error()}, nil
}
// 同步清理禁用表
if Reg != nil {
Reg.EnablePlugin(name)
}
return map[string]interface{}{
"status": "removed",
"name": name,

View File

@ -107,6 +107,7 @@ tr:hover td { background:var(--bg-hover) }
.badge-red { background:#7f1d1d; color:#fca5a5 }
.badge-yellow { background:#713f12; color:#fde68a }
.badge-blue { background:#1e3a5f; color:#93c5fd }
.badge-gray { background:#374151; color:#9ca3af }
.btn { padding:6px 14px; border-radius:6px; border:none; font-size:12px; cursor:pointer; font-weight:500; transition:all .15s }
.btn-primary { background:var(--accent); color:#fff }
.btn-primary:hover { filter:brightness(1.1) }
@ -312,6 +313,7 @@ let state = {
settings: {},
meta: {},
pluginMeta: {},
disabledPlugins: [],
settingsPlugins: ['core'],
selectedSection: 'core',
messages: [],
@ -439,6 +441,7 @@ async function renderAll() {
state.meta = s.meta || {};
state.settingsPlugins = s.plugins || ['core'];
state.pluginMeta = s.plugin_meta || {};
state.disabledPlugins = s.disabled_plugins || [];
} catch(e) {}
try { state.installedPlugins = await api('/plugins') } catch(e) {}
try { await loadTerminals() } catch(e) {}
@ -1223,17 +1226,38 @@ function renderPlugins() {
+ '<button class="btn btn-primary" onclick="installPlugin()">' + __('安装','Install') + '</button></div>'
+ '<div><input type="file" id="plugin-file" accept=".hmap" style="display:inline;width:auto" onchange="installPluginFile(this.files[0])">'
+ '<label for="plugin-file" class="btn btn-ghost" style="cursor:pointer">' + __('选择 .hmap 文件上传','Upload .hmap file') + '</label></div></div>';
var disabledNames = {};
(state.disabledPlugins || []).forEach(function(d) { disabledNames[d.name] = d });
var installedNames = (state.installedPlugins || []).map(function(p) { return p.name });
html += '<div class="card"><h2>' + __('已加载插件','Loaded Plugins') + ' (' + plugins.length + ')</h2>';
if (plugins.length === 0) {
html += '<div class="empty-state"><p>' + __('暂无已加载插件','No loaded plugins') + '</p></div>';
var allPluginNames = {};
plugins.forEach(function(p) { allPluginNames[p.name] = true });
state.disabledPlugins.forEach(function(d) { allPluginNames[d.name] = true });
html += '<div class="card"><h2>' + __('所有插件','All Plugins') + ' (' + Object.keys(allPluginNames).length + ')</h2>';
var names = Object.keys(allPluginNames).sort();
if (names.length === 0) {
html += '<div class="empty-state"><p>' + __('暂无插件','No plugins') + '</p></div>';
} else {
html += '<table><tr><th>' + __('名称','Name') + '</th><th>' + __('状态','Status') + '</th><th>' + __('操作','Actions') + '</th></tr>';
plugins.forEach(function(p) {
var isExternal = installedNames.indexOf(p.name) >= 0;
html += '<tr><td>' + escHtml(p.name) + '</td>'
+ '<td><span class="badge badge-green">' + __('已加载','Loaded') + '</span></td>'
+ '<td>' + (isExternal ? '<button class="btn btn-sm btn-danger" onclick="removePlugin(\'' + escHtml(p.name) + '\')">' + __('卸载','Unload') + '</button>' : '<span class="badge badge-blue">' + __('内置','Built-in') + '</span>') + '</td></tr>';
names.forEach(function(name) {
var isExternal = installedNames.indexOf(name) >= 0;
var isDisabled = disabledNames[name];
var loaded = plugins.some(function(p) { return p.name === name });
var statusHtml = loaded && !isDisabled ? '<span class="badge badge-green">' + __('已加载','Loaded') + '</span>'
: loaded && isDisabled ? '<span class="badge badge-yellow">' + __('运行中(禁用待生效)','Running (disable pending)') + '</span>'
: isDisabled ? '<span class="badge badge-red">' + __('已禁用','Disabled') + '</span>'
: '<span class="badge badge-gray">' + __('未加载','Not Loaded') + '</span>';
var actionsHtml = '';
if (loaded) {
actionsHtml += '<button class="btn btn-sm btn-warning" onclick="disablePlugin(\'' + escHtml(name) + '\')" style="margin-right:4px">' + __('禁用','Disable') + '</button>';
} else {
actionsHtml += '<button class="btn btn-sm btn-success" onclick="enablePlugin(\'' + escHtml(name) + '\')" style="margin-right:4px">' + __('启用','Enable') + '</button>';
}
if (isExternal) {
actionsHtml += '<button class="btn btn-sm btn-danger" onclick="removePlugin(\'' + escHtml(name) + '\')">' + __('卸载','Unload') + '</button>';
}
html += '<tr><td>' + escHtml(name) + '</td>'
+ '<td>' + statusHtml + '</td>'
+ '<td>' + actionsHtml + '</td></tr>';
});
html += '</table>';
}
@ -1319,6 +1343,32 @@ async function removePlugin(name) {
} catch(e) { toast(__('卸载失败: ','Unload failed: ') + e.message, true) }
}
async function disablePlugin(name) {
if (name === 'webui') {
var r = confirm(__('禁用 WebUI 后将无法通过 URL:端口访问此管理面板,若要重新启用需要通过 CLI 命令 /plugin enable webui 恢复。\n\n确定要禁用吗','Disabling WebUI will make this management panel inaccessible via URL:port. To re-enable, use CLI command /plugin enable webui.\n\nAre you sure?'));
if (!r) return;
}
try {
var r = await api('/plugins/' + encodeURIComponent(name) + '/disable', { method: 'POST' });
toast(__('已禁用: ','Disabled: ') + name);
state.kernel = await api('/kernel');
var s = await api('/settings');
state.disabledPlugins = s.disabled_plugins || [];
renderPlugins();
} catch(e) { toast(__('禁用失败: ','Disable failed: ') + e.message, true) }
}
async function enablePlugin(name) {
try {
var r = await api('/plugins/' + encodeURIComponent(name) + '/enable', { method: 'POST' });
toast(__('已启用: ','Enabled: ') + name);
state.kernel = await api('/kernel');
var s = await api('/settings');
state.disabledPlugins = s.disabled_plugins || [];
renderPlugins();
} catch(e) { toast(__('启用失败: ','Enable failed: ') + e.message, true) }
}
async function reloadPlugins() {
try {
var r = await api('/plugins/reload', { method: 'POST' });

View File

@ -27,6 +27,7 @@ import (
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/text"
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
"gitcode.com/JianFeeeee/HomeAgent/internal/skill"
"gitcode.com/JianFeeeee/HomeAgent/internal/supervisor"
"gitcode.com/JianFeeeee/HomeAgent/internal/tracker"
@ -61,6 +62,7 @@ type Handler struct {
tracker *tracker.Tracker
cfgReg *internalConfig.ConfigRegistry
pluginReg *plugin.Registry
pluginMgr sdk.PluginManager
eventBus *events.Bus
statusProvider agentCore.StatusProvider
providerMgr *agentAPI.ProviderManager
@ -102,6 +104,8 @@ type termState struct {
created time.Time
}
func (h *Handler) SetPluginMgr(mgr sdk.PluginManager) { h.pluginMgr = mgr }
const maxChatHistory = 200
const maxCmdHistory = 100
const maxTerminals = 50
@ -1106,12 +1110,17 @@ func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) {
plugins = append(plugins, "plugin."+p)
}
}
var disabledPlugins []sdk.DisabledPluginInfo
if h.pluginMgr != nil {
disabledPlugins = h.pluginMgr.ListDisabledPlugins()
}
sort.Strings(plugins)
writeJSON(w, http.StatusOK, map[string]interface{}{
"settings": values,
"meta": meta,
"plugins": plugins,
"plugin_meta": pm,
"settings": values,
"meta": meta,
"plugins": plugins,
"plugin_meta": pm,
"disabled_plugins": disabledPlugins,
})
case http.MethodPut:
var body struct {
@ -1417,6 +1426,15 @@ func (h *Handler) handlePluginByID(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/api/v1/plugins/")
path = strings.TrimSuffix(path, "/")
if path == "disabled" && r.Method == http.MethodGet {
if h.pluginMgr == nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "plugin manager not available"})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{"disabled": h.pluginMgr.ListDisabledPlugins()})
return
}
if path == "reload" && r.Method == http.MethodPost {
if h.pluginReg == nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "plugin registry not available"})
@ -1436,6 +1454,38 @@ func (h *Handler) handlePluginByID(w http.ResponseWriter, r *http.Request) {
return
}
if idx := strings.LastIndex(path, "/"); idx > 0 {
name := path[:idx]
action := path[idx+1:]
if r.Method == http.MethodPost {
switch action {
case "disable":
if h.pluginMgr == nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "plugin manager not available"})
return
}
if err := h.pluginMgr.DisablePlugin(name, "webui"); err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "disabled"})
return
case "enable":
if h.pluginMgr == nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "plugin manager not available"})
return
}
if err := h.pluginMgr.EnablePlugin(name); err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "enabled"})
return
}
}
}
switch r.Method {
case http.MethodGet:
h.proxyToPluginmgr(w, r, "/plugins/"+path)

View File

@ -195,6 +195,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
})
h := NewHandler(p.sup, p.mem, p.sk, p.lua, p.cfg, p.iom, p.tm, p.ks, p.tr, p.cr, p.pr, p.evBus, p.statusProvider, p.providerMgr, p.baseAPIKey)
h.SetPluginMgr(s.PluginMgr())
p.handler = h
h.RegisterRoutes(p.mux)

View File

@ -51,13 +51,31 @@ type OutputChannelRegistrar = pubsdk.OutputChannelRegistrar
type InputChannelRegistrar = pubsdk.InputChannelRegistrar
type ChannelDef = pubsdk.ChannelDef
type DisabledPluginInfo struct {
Name string `json:"name"`
DisabledAt string `json:"disabled_at"`
DisabledBy string `json:"disabled_by"`
}
type PluginManager interface {
ListLoadedPlugins() []string
ListDisabledPlugins() []DisabledPluginInfo
IsPluginDisabled(name string) bool
DisablePlugin(name, by string) error
EnablePlugin(name string) error
ReloadPlugins() (string, error)
}
type PluginSDK struct {
*pubsdk.PluginSDK
iom *agentIO.IOManager
eventBus *events.Bus
logger *log.Logger
iom *agentIO.IOManager
eventBus *events.Bus
logger *log.Logger
pluginMgr PluginManager
}
func (s *PluginSDK) PluginMgr() PluginManager { return s.pluginMgr }
// ioAdapter 桥接 IOManager 到公共 SDK 的 IOInjector 接口,
// 确保外部插件通过 s.InjectText() 等方法的调用能被路由到内核 IO 层。
type ioAdapter struct{ iom *agentIO.IOManager }
@ -95,6 +113,7 @@ type SDKConfig struct {
RegAPI APIRegistrar
RegOutput OutputChannelRegistrar
RegInput InputChannelRegistrar
PluginMgr PluginManager
}
func New(name string, cfg SDKConfig) *PluginSDK {
@ -115,6 +134,7 @@ func New(name string, cfg SDKConfig) *PluginSDK {
iom: cfg.IOManager,
eventBus: cfg.EventBus,
logger: log.Default(),
pluginMgr: cfg.PluginMgr,
}
}