feat: plugin install/uninstall lifecycle + UI buttons

- AppConfig: add InstalledPlugins []string
- Manager.Discover(): no config dependency, all plugins start inactive
- Manager.SyncConfig(): apply installed/enabled state from AppConfig
- Manager.Enable(): works for plugins without on_install hook
- Manager.Install/Uninstall(): run on_install/on_uninstall hooks
- ActivatePlugin: skip if HasInstall && !Installed
- ReloadPlugins: Discover → SyncConfig → InitRuntimes
- Bindings: InstallPlugin, UninstallPlugin
- SettingsPlugins: install/uninstall buttons, toggle only after install
- Calendar: migration moved from on_init to on_install, on_uninstall drops tables
- Tests: all 12 pass (manager + runtime + calendar)
This commit is contained in:
2026-06-07 15:28:37 +08:00
parent b80941f908
commit e99ff984b1
10 changed files with 1358 additions and 161 deletions
+38 -10
View File
@@ -18,6 +18,8 @@ type PluginDTO struct {
Author string `json:"author,omitempty"`
Description string `json:"description,omitempty"`
Active bool `json:"active"`
Installed bool `json:"installed"`
HasInstall bool `json:"hasInstall"`
HasPanel bool `json:"hasPanel"`
HasSettings bool `json:"hasSettings"`
UIContribs UIContribDTO `json:"uiContribs"`
@@ -89,6 +91,8 @@ func (a *App) ListPlugins() []PluginDTO {
Author: p.Meta.Author,
Description: p.Meta.Description,
Active: active,
Installed: p.Installed,
HasInstall: p.HasInstall,
HasPanel: hasPanel,
UIContribs: contribs,
})
@@ -97,11 +101,22 @@ func (a *App) ListPlugins() []PluginDTO {
}
// SetPluginEnabled persists the enabled/disabled state and applies it to the runtime.
// Returns error if the plugin is not installed but has install lifecycle.
func (a *App) SetPluginEnabled(name string, enabled bool) error {
if a.plugins == nil {
return fmt.Errorf("plugin manager not ready")
}
if enabled {
if err := a.plugins.Enable(name); err != nil {
return err
}
} else {
if err := a.plugins.Disable(name); err != nil {
return err
}
}
appCfg, _ := config.LoadAppConfig()
if appCfg == nil {
appCfg = config.DefaultAppConfig()
@@ -207,26 +222,39 @@ func (a *App) ReloadPlugins() error {
log.Print("[plugins] reload requested")
a.plugins.CloseRuntimes()
appCfg, _ := config.LoadAppConfig()
if appCfg == nil {
appCfg = config.DefaultAppConfig()
}
enabledSet := make(map[string]bool)
for _, name := range appCfg.EnabledPlugins {
enabledSet[name] = true
}
a.plugins.Discover()
a.plugins.InitRuntimes()
appCfg, _ := config.LoadAppConfig()
a.plugins.SyncConfig(appCfg)
// Apply enable/disable state from config: deactivate everything not in enabled set
for _, p := range a.plugins.Plugins() {
if !enabledSet[p.Meta.Name] {
if !p.Active || !p.Installed {
a.plugins.DeactivatePlugin(p.Meta.Name)
}
}
a.plugins.InitRuntimes()
a.plugins.CallInitHooks()
a.plugins.StartSchedulers()
log.Print("[plugins] reload complete")
return nil
}
// InstallPlugin creates plugin's database tables and defaults via on_install hook.
// Does NOT activate the plugin — use SetPluginEnabled after.
func (a *App) InstallPlugin(name string) error {
if a.plugins == nil {
return fmt.Errorf("plugin manager not ready")
}
return a.plugins.Install(name)
}
// UninstallPlugin drops plugin's database tables and cleans data via on_uninstall hook.
// Disables the plugin first if active. Does NOT delete plugin files from disk.
func (a *App) UninstallPlugin(name string) error {
if a.plugins == nil {
return fmt.Errorf("plugin manager not ready")
}
return a.plugins.Uninstall(name)
}