Prune stale plugin metadata

This commit is contained in:
npc0-hue
2026-09-14 17:32:51 +08:00
parent ecdca8e28b
commit 63e2340db7
10 changed files with 659 additions and 13 deletions
+147 -7
View File
@@ -728,10 +728,7 @@ func (svc *CoreService) CreateGamePlugin(plugin domain.GamePlugin) (domain.GameP
if err := validator.ValidateGamePlugin(plugin); err != nil {
return domain.GamePlugin{}, err
}
if err := svc.store.GamePlugins().Create(plugin); err != nil {
return domain.GamePlugin{}, err
}
return domain.CopyGamePlugin(plugin), nil
return svc.upsertLatestGamePlugin(plugin)
}
func (svc *CoreService) RegisterGamePluginManifest(registration domain.GamePluginManifestRegistration) (domain.GamePlugin, error) {
@@ -743,11 +740,35 @@ func (svc *CoreService) RegisterGamePluginManifest(registration domain.GamePlugi
if err := validator.ValidateGamePlugin(plugin); err != nil {
return domain.GamePlugin{}, err
}
if existing, err := svc.store.GamePlugins().Get(plugin.ID); err == nil {
if err := svc.store.GamePlugins().Update(plugin); err != nil {
return svc.upsertLatestGamePlugin(plugin)
}
func (svc *CoreService) upsertLatestGamePlugin(plugin domain.GamePlugin) (domain.GamePlugin, error) {
plugins, err := svc.store.GamePlugins().List(domain.GamePluginFilter{})
if err != nil {
return domain.GamePlugin{}, err
}
identity := domain.GamePluginIdentityKey(plugin)
var current domain.GamePlugin
matched := make([]domain.GamePlugin, 0, len(plugins))
for _, candidate := range plugins {
if domain.GamePluginIdentityKey(candidate) != identity {
continue
}
matched = append(matched, candidate)
if current.ID == "" || domain.GamePluginIsNewer(candidate, current) {
current = candidate
}
}
if current.ID != "" && !domain.GamePluginIsNewer(plugin, current) {
if err := svc.pruneOlderGamePluginVersions(current, matched); err != nil {
return domain.GamePlugin{}, err
}
if err := svc.refreshServerPluginReferences(existing.ID, plugin.Version); err != nil {
return domain.CopyGamePlugin(current), nil
}
if _, err := svc.store.GamePlugins().Get(plugin.ID); err == nil {
if err := svc.store.GamePlugins().Update(plugin); err != nil {
return domain.GamePlugin{}, err
}
} else if errors.Is(err, repo.ErrNotFound) {
@@ -757,9 +778,128 @@ func (svc *CoreService) RegisterGamePluginManifest(registration domain.GamePlugi
} else {
return domain.GamePlugin{}, err
}
for _, stale := range matched {
if stale.ID == plugin.ID {
continue
}
if err := svc.replaceGamePluginReference(stale.ID, plugin.ID, plugin.Version); err != nil {
return domain.GamePlugin{}, err
}
if err := svc.store.GamePlugins().Delete(stale.ID); err != nil && !errors.Is(err, repo.ErrNotFound) {
return domain.GamePlugin{}, err
}
}
if err := svc.refreshServerPluginReferences(plugin.ID, plugin.Version); err != nil {
return domain.GamePlugin{}, err
}
return domain.CopyGamePlugin(plugin), nil
}
func (svc *CoreService) pruneOlderGamePluginVersions(latest domain.GamePlugin, plugins []domain.GamePlugin) error {
for _, plugin := range plugins {
if plugin.ID == latest.ID {
continue
}
if err := svc.replaceGamePluginReference(plugin.ID, latest.ID, latest.Version); err != nil {
return err
}
if err := svc.store.GamePlugins().Delete(plugin.ID); err != nil && !errors.Is(err, repo.ErrNotFound) {
return err
}
}
return svc.refreshServerPluginReferences(latest.ID, latest.Version)
}
func (svc *CoreService) replaceGamePluginReference(fromPluginID, toPluginID, toPluginVersion string) error {
if strings.TrimSpace(fromPluginID) == "" || fromPluginID == toPluginID {
return nil
}
stamp := svc.now()
instances, err := svc.store.ServerInstances().List(domain.ServerInstanceFilter{PluginID: fromPluginID})
if err != nil {
return err
}
for _, instance := range instances {
instance.PluginID = toPluginID
instance.PluginVersion = toPluginVersion
instance.UpdatedAt = stamp
if err := validator.ValidateStoredServerInstance(instance); err != nil {
return err
}
if err := svc.store.ServerInstances().Update(instance); err != nil {
return err
}
}
bindings, err := svc.store.RuntimeBindings().List(domain.RuntimeBindingFilter{})
if err != nil {
return err
}
for _, binding := range bindings {
if binding.PluginID != fromPluginID {
continue
}
binding.PluginID = toPluginID
binding.PluginVersion = toPluginVersion
binding.UpdatedAt = stamp
if err := svc.store.RuntimeBindings().Update(binding); err != nil {
return err
}
}
installations, err := svc.store.PluginLifecycles().List(domain.PluginLifecycleFilter{PluginID: fromPluginID})
if err != nil {
return err
}
for _, installation := range installations {
installation.PluginID = toPluginID
if installation.TargetVersion != "" {
installation.TargetVersion = toPluginVersion
}
installation.UpdatedAt = stamp
if err := svc.store.PluginLifecycles().Update(installation); err != nil {
return err
}
}
previews, err := svc.store.AIConfigDiffs().List(domain.AIConfigDiffFilter{PluginID: fromPluginID})
if err != nil {
return err
}
for _, preview := range previews {
preview.PluginID = toPluginID
preview.UpdatedAt = stamp
if err := svc.store.AIConfigDiffs().Update(preview); err != nil {
return err
}
}
return svc.replacePluginDataReferences(fromPluginID, toPluginID)
}
func (svc *CoreService) replacePluginDataReferences(fromPluginID, toPluginID string) error {
items, err := svc.store.PluginDataRecords().List(domain.PluginDataFilter{PluginID: fromPluginID})
if err != nil {
return err
}
upserts := make([]domain.PluginDataRecord, 0, len(items))
deleteIDs := make([]string, 0, len(items))
for _, item := range items {
deleteIDs = append(deleteIDs, item.ID)
if legacySCUMPluginDataCollection(fromPluginID, item.Collection) {
continue
}
item.PluginID = toPluginID
item.ID = pluginDataID(item.ServerInstanceID, toPluginID, item.Collection, item.Key)
if existing, getErr := svc.store.PluginDataRecords().Get(item.ID); getErr == nil && existing.UpdatedAt.After(item.UpdatedAt) {
continue
} else if getErr != nil && !errors.Is(getErr, repo.ErrNotFound) {
return getErr
}
upserts = append(upserts, item)
}
if len(upserts) == 0 && len(deleteIDs) == 0 {
return nil
}
return svc.store.PluginDataRecords().Apply(upserts, deleteIDs)
}
// refreshServerPluginReferences keeps existing server projections usable when a
// manifest is refreshed in place. The server and its logical runtime binding
// carry the manifest version used for lifecycle validation; leaving either at a