mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 01:48:11 +00:00
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:
@ -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) {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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"})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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' });
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user