feat(plugins): 插件管理 - 区分系统与官方插件权限 (#329)
This commit is contained in:
@@ -39,6 +39,8 @@ func Models() []any {
|
||||
&model.AdminAuditEvent{},
|
||||
&model.UserDailyActivity{},
|
||||
&model.SystemSetting{},
|
||||
&model.PluginPlatformState{},
|
||||
&model.UserPluginState{},
|
||||
&model.ArkPrivateAssetBinding{},
|
||||
&model.UserOSSSetting{},
|
||||
&model.UserDailyUploadUsage{},
|
||||
|
||||
@@ -38,7 +38,7 @@ func RegisterComfyBridgeRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
if err := svc.RequireWorkflowPluginForInterface("comfyui-bridge-image"); err != nil {
|
||||
if err := svc.RequireWorkflowPluginForUser(user.ID, "comfyui-bridge-image"); err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func RegisterComfyBridgeRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
if err := svc.RequireWorkflowPluginForInterface("comfyui-bridge-image"); err != nil {
|
||||
if err := svc.RequireWorkflowPluginForUser(user.ID, "comfyui-bridge-image"); err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
@@ -78,10 +78,6 @@ func RegisterComfyBridgeRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
if err := svc.RequireWorkflowPluginForInterface("comfyui-bridge-image"); err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
if err := svc.RevokeComfyBridge(user.ID, c.Param("id")); err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
@@ -97,10 +93,6 @@ func RegisterComfyBridgeRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
if err := svc.RequireWorkflowPluginForInterface("comfyui-bridge-image"); err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
wait, err := comfyBridgeWait(c.Query("wait"))
|
||||
if err != nil {
|
||||
fail(c, http.StatusBadRequest, err)
|
||||
@@ -122,10 +114,6 @@ func RegisterComfyBridgeRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
if err := svc.RequireWorkflowPluginForInterface("comfyui-bridge-image"); err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
if c.Request.ContentLength > maxComfyBridgeRequestBytes {
|
||||
fail(c, http.StatusRequestEntityTooLarge, errors.New("Bridge 结果请求体超过 64MB"))
|
||||
return
|
||||
@@ -163,10 +151,6 @@ func RegisterComfyBridgeRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
if err := svc.RequireWorkflowPluginForInterface("comfyui-bridge-image"); err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Capabilities map[string]any `json:"capabilities,omitempty"`
|
||||
}
|
||||
|
||||
@@ -16,11 +16,56 @@ import (
|
||||
func RegisterPluginRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
statusRoutes := r.Group("/plugins")
|
||||
statusRoutes.GET("/status", func(c *gin.Context) {
|
||||
if _, err := currentUser(c, svc); err != nil {
|
||||
user, err := currentUser(c, svc)
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
ok(c, gin.H{"statuses": svc.WorkflowPluginStatuses()})
|
||||
states, err := svc.PluginStatesForUser(user)
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
statuses, err := svc.WorkflowPluginStatusesForUser(user.ID)
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
ok(c, gin.H{"statuses": statuses, "states": states})
|
||||
})
|
||||
adminRoutes := r.Group("/admin/plugins")
|
||||
adminRoutes.GET("", func(c *gin.Context) {
|
||||
user, err := currentUser(c, svc)
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
states, err := svc.AdminPluginStates(user)
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
ok(c, gin.H{"plugins": svc.Plugins(), "states": states})
|
||||
})
|
||||
adminRoutes.PUT("/:id/availability", func(c *gin.Context) {
|
||||
user, err := currentUser(c, svc)
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
Available bool `json:"available"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
fail(c, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
state, err := svc.SetPluginPlatformAvailability(user, c.Param("id"), request.Available)
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
ok(c, gin.H{"state": state})
|
||||
})
|
||||
pluginRoutes := r.Group("/plugins")
|
||||
pluginRoutes.Use(requirePluginCenterAccess(svc))
|
||||
@@ -37,7 +82,12 @@ func RegisterPluginRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
ok(c, gin.H{"plugins": plugins})
|
||||
states, err := svc.PluginStatesForUser(user)
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
ok(c, gin.H{"plugins": plugins, "states": states})
|
||||
})
|
||||
pluginRoutes.GET("/catalog", func(c *gin.Context) {
|
||||
if _, err := currentUser(c, svc); err != nil {
|
||||
@@ -77,7 +127,7 @@ func RegisterPluginRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
fail(c, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
plugin, err := svc.InstallPlugin(data, fileName)
|
||||
plugin, err := svc.InstallPluginForAdmin(user, data, fileName)
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
@@ -85,10 +135,27 @@ func RegisterPluginRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
ok(c, gin.H{"plugin": plugin})
|
||||
})
|
||||
pluginRoutes.GET("/:id/package", func(c *gin.Context) {
|
||||
if _, err := currentUser(c, svc); err != nil {
|
||||
user, err := currentUser(c, svc)
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
visiblePlugins, err := svc.PluginsForUser(user)
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
visible := false
|
||||
for _, plugin := range visiblePlugins {
|
||||
if plugin.Manifest.ID == c.Param("id") {
|
||||
visible = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !visible {
|
||||
failService(c, service.Forbidden("无权访问该插件包"))
|
||||
return
|
||||
}
|
||||
data, fileName, err := svc.PluginPackage(c.Param("id"))
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
@@ -103,6 +170,26 @@ func RegisterPluginRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
})
|
||||
pluginRoutes.POST("/:id/enable", pluginToggle(svc, true))
|
||||
pluginRoutes.POST("/:id/disable", pluginToggle(svc, false))
|
||||
pluginRoutes.PUT("/:id/activation", func(c *gin.Context) {
|
||||
user, err := currentUser(c, svc)
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
fail(c, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
state, err := svc.SetUserPluginEnabled(user, c.Param("id"), request.Enabled)
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
ok(c, gin.H{"state": state})
|
||||
})
|
||||
pluginRoutes.DELETE("/:id", func(c *gin.Context) {
|
||||
user, err := currentUser(c, svc)
|
||||
if err != nil {
|
||||
@@ -113,7 +200,7 @@ func RegisterPluginRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
if err := svc.UninstallPlugin(c.Param("id")); err != nil {
|
||||
if err := svc.UninstallPluginForAdmin(user, c.Param("id")); err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
@@ -121,8 +208,7 @@ func RegisterPluginRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
})
|
||||
|
||||
pluginRoutes.GET("/eagle/library", func(c *gin.Context) {
|
||||
if _, err := currentUser(c, svc); err != nil {
|
||||
failService(c, err)
|
||||
if _, allowed := requireEnabledPlugin(c, svc, service.PluginEagleAssetConnector); !allowed {
|
||||
return
|
||||
}
|
||||
library, err := svc.EagleLibrary(c.Query("baseUrl"))
|
||||
@@ -134,8 +220,7 @@ func RegisterPluginRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
ok(c, gin.H{"library": library})
|
||||
})
|
||||
pluginRoutes.GET("/eagle/items", func(c *gin.Context) {
|
||||
if _, err := currentUser(c, svc); err != nil {
|
||||
failService(c, err)
|
||||
if _, allowed := requireEnabledPlugin(c, svc, service.PluginEagleAssetConnector); !allowed {
|
||||
return
|
||||
}
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "60"))
|
||||
@@ -148,8 +233,7 @@ func RegisterPluginRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
ok(c, gin.H{"items": items})
|
||||
})
|
||||
pluginRoutes.GET("/eagle/items/:itemId/file", func(c *gin.Context) {
|
||||
if _, err := currentUser(c, svc); err != nil {
|
||||
failService(c, err)
|
||||
if _, allowed := requireEnabledPlugin(c, svc, service.PluginEagleAssetConnector); !allowed {
|
||||
return
|
||||
}
|
||||
file, err := svc.OpenEagleItemFile(c.Query("baseUrl"), c.Param("itemId"))
|
||||
@@ -164,8 +248,7 @@ func RegisterPluginRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
c.DataFromReader(http.StatusOK, file.Size, file.MimeType, file.Body, nil)
|
||||
})
|
||||
pluginRoutes.GET("/eagle/items/:itemId/thumbnail", func(c *gin.Context) {
|
||||
if _, err := currentUser(c, svc); err != nil {
|
||||
failService(c, err)
|
||||
if _, allowed := requireEnabledPlugin(c, svc, service.PluginEagleAssetConnector); !allowed {
|
||||
return
|
||||
}
|
||||
file, err := svc.OpenEagleItemThumbnail(c.Query("baseUrl"), c.Param("itemId"))
|
||||
@@ -179,8 +262,7 @@ func RegisterPluginRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
c.DataFromReader(http.StatusOK, file.Size, file.MimeType, file.Body, nil)
|
||||
})
|
||||
pluginRoutes.POST("/eagle/items", func(c *gin.Context) {
|
||||
if _, err := currentUser(c, svc); err != nil {
|
||||
failService(c, err)
|
||||
if _, allowed := requireEnabledPlugin(c, svc, service.PluginEagleAssetConnector); !allowed {
|
||||
return
|
||||
}
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 160<<20)
|
||||
@@ -197,8 +279,7 @@ func RegisterPluginRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
ok(c, gin.H{"item": item})
|
||||
})
|
||||
pluginRoutes.POST("/eagle/folders", func(c *gin.Context) {
|
||||
if _, err := currentUser(c, svc); err != nil {
|
||||
failService(c, err)
|
||||
if _, allowed := requireEnabledPlugin(c, svc, service.PluginEagleAssetConnector); !allowed {
|
||||
return
|
||||
}
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 16<<10)
|
||||
@@ -250,11 +331,23 @@ func pluginToggle(svc *service.Service, enabled bool) gin.HandlerFunc {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
plugin, err := svc.SetPluginEnabled(c.Param("id"), enabled)
|
||||
state, err := svc.SetPluginPlatformAvailability(user, c.Param("id"), enabled)
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
ok(c, gin.H{"plugin": plugin})
|
||||
ok(c, gin.H{"state": state})
|
||||
}
|
||||
}
|
||||
|
||||
func requireEnabledPlugin(c *gin.Context, svc *service.Service, pluginID string) (*model.User, bool) {
|
||||
user, err := currentUser(c, svc)
|
||||
if err == nil {
|
||||
err = svc.RequirePluginForUser(user.ID, pluginID)
|
||||
}
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return nil, false
|
||||
}
|
||||
return user, true
|
||||
}
|
||||
|
||||
@@ -11,11 +11,12 @@ import (
|
||||
// RegisterRunningHubRoutes 是独立工作流 Provider 的管理代理,不复用 ModelChannel。
|
||||
func RegisterRunningHubRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
fetch := func(c *gin.Context, app bool) {
|
||||
if _, err := currentUser(c, svc); err != nil {
|
||||
user, err := currentUser(c, svc)
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
if err := svc.RequireWorkflowPluginForInterface("runninghub-workflow-image"); err != nil {
|
||||
if err := svc.RequireWorkflowPluginForUser(user.ID, "runninghub-workflow-image"); err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
@@ -26,7 +27,6 @@ func RegisterRunningHubRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
return
|
||||
}
|
||||
var result map[string]any
|
||||
var err error
|
||||
if app {
|
||||
result, err = svc.FetchRunningHubAppInfo(c.Request.Context(), req)
|
||||
} else {
|
||||
|
||||
24
backend/internal/model/models_plugin.go
Normal file
24
backend/internal/model/models_plugin.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// PluginPlatformState stores the administrator-controlled availability of a
|
||||
// plugin. User activation can never make an unavailable plugin effective.
|
||||
type PluginPlatformState struct {
|
||||
PluginID string `json:"pluginId" gorm:"primaryKey;size:120"`
|
||||
Available bool `json:"available" gorm:"index"`
|
||||
UpdatedBy string `json:"updatedBy" gorm:"index;size:36"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// UserPluginState stores one user's activation choice for an application
|
||||
// plugin. System-scoped protocol and uploaded plugins do not use this table.
|
||||
type UserPluginState struct {
|
||||
ID string `json:"id" gorm:"primaryKey;size:36"`
|
||||
UserID string `json:"userId" gorm:"size:36;index;uniqueIndex:idx_user_plugin_state_user_plugin,priority:1"`
|
||||
PluginID string `json:"pluginId" gorm:"size:120;index;uniqueIndex:idx_user_plugin_state_user_plugin,priority:2"`
|
||||
Enabled bool `json:"enabled" gorm:"index"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
86
backend/internal/repository/plugin_states.go
Normal file
86
backend/internal/repository/plugin_states.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"infinite-canvas/backend/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func (r *Repository) PluginPlatformState(pluginID string) (*model.PluginPlatformState, error) {
|
||||
var state model.PluginPlatformState
|
||||
if err := r.db.First(&state, "plugin_id = ?", pluginID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
func (r *Repository) PluginPlatformStates() ([]model.PluginPlatformState, error) {
|
||||
var states []model.PluginPlatformState
|
||||
err := r.db.Order("plugin_id asc").Find(&states).Error
|
||||
return states, err
|
||||
}
|
||||
|
||||
func (r *Repository) SavePluginPlatformState(state *model.PluginPlatformState) error {
|
||||
return r.db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "plugin_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"available", "updated_by", "updated_at"}),
|
||||
}).Create(state).Error
|
||||
}
|
||||
|
||||
func (r *Repository) DeletePluginPlatformState(pluginID string) error {
|
||||
return r.db.Delete(&model.PluginPlatformState{}, "plugin_id = ?", pluginID).Error
|
||||
}
|
||||
|
||||
func (r *Repository) UserPluginState(userID string, pluginID string) (*model.UserPluginState, error) {
|
||||
var state model.UserPluginState
|
||||
if err := r.db.First(&state, "user_id = ? AND plugin_id = ?", userID, pluginID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
func (r *Repository) UserPluginStates(userID string) ([]model.UserPluginState, error) {
|
||||
var states []model.UserPluginState
|
||||
err := r.db.Where("user_id = ?", userID).Order("plugin_id asc").Find(&states).Error
|
||||
return states, err
|
||||
}
|
||||
|
||||
func (r *Repository) SaveUserPluginState(state *model.UserPluginState) error {
|
||||
return r.db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "user_id"}, {Name: "plugin_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"enabled", "updated_at"}),
|
||||
}).Create(state).Error
|
||||
}
|
||||
|
||||
func (r *Repository) DeleteUserPluginStates(pluginID string) error {
|
||||
return r.db.Delete(&model.UserPluginState{}, "plugin_id = ?", pluginID).Error
|
||||
}
|
||||
|
||||
func (r *Repository) EnabledPluginUserCounts() (map[string]int64, error) {
|
||||
type countRow struct {
|
||||
PluginID string
|
||||
Count int64
|
||||
}
|
||||
var rows []countRow
|
||||
if err := r.db.Model(&model.UserPluginState{}).
|
||||
Select("plugin_id, COUNT(*) AS count").
|
||||
Where("enabled = ?", true).
|
||||
Group("plugin_id").
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[string]int64, len(rows))
|
||||
for _, row := range rows {
|
||||
result[row.PluginID] = row.Count
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func TestUpdateFeatureAvailabilityPersistsAndAudits(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginsForUserHidesBundledPluginsOnlyForOrdinaryUsers(t *testing.T) {
|
||||
func TestPluginsForUserKeepsOfficialApplicationsAndHidesManagedPlugins(t *testing.T) {
|
||||
svc, _ := newFeatureAvailabilityTestService(t)
|
||||
admin := &model.User{ID: "admin-1", Role: model.UserRoleAdmin}
|
||||
if _, err := svc.UpdateFeatureAvailability(admin, FeatureAvailability{
|
||||
@@ -83,21 +83,22 @@ func TestPluginsForUserHidesBundledPluginsOnlyForOrdinaryUsers(t *testing.T) {
|
||||
}
|
||||
svc.pluginRuntime = &pluginRuntime{plugins: map[string]pluginRecord{
|
||||
"bundled": {Source: "bundled", Metadata: protocol.Metadata{ID: "bundled", Name: "系统协议", Version: "1"}},
|
||||
"uploaded": {Source: "uploaded", Metadata: protocol.Metadata{ID: "uploaded", Name: "用户协议", Version: "1"}},
|
||||
"uploaded": {Source: "uploaded", Metadata: protocol.Metadata{ID: "uploaded", Name: "自定义协议", Version: "1"}},
|
||||
"app": {Source: "bundled", Metadata: protocol.Metadata{ID: WorkflowPluginRunningHub, Name: "官方应用", Version: "1"}},
|
||||
}}
|
||||
|
||||
visibleToUser, err := svc.PluginsForUser(&model.User{ID: "user-1", Role: model.UserRoleUser})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(visibleToUser) != 1 || visibleToUser[0].Source != "uploaded" {
|
||||
if len(visibleToUser) != 1 || visibleToUser[0].Manifest.ID != WorkflowPluginRunningHub {
|
||||
t.Fatalf("PluginsForUser(user) = %#v", visibleToUser)
|
||||
}
|
||||
visibleToAdmin, err := svc.PluginsForUser(admin)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(visibleToAdmin) != 2 {
|
||||
if len(visibleToAdmin) != 3 {
|
||||
t.Fatalf("PluginsForUser(admin) = %#v", visibleToAdmin)
|
||||
}
|
||||
}
|
||||
|
||||
303
backend/internal/service/plugin_management.go
Normal file
303
backend/internal/service/plugin_management.go
Normal file
@@ -0,0 +1,303 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"infinite-canvas/backend/internal/model"
|
||||
)
|
||||
|
||||
const (
|
||||
PluginEagleAssetConnector = "eagle-asset-connector"
|
||||
PluginPromptOptimizer = "prompt-optimizer"
|
||||
PluginPortraitClearance = "portrait-clearance"
|
||||
|
||||
PluginOriginOfficial = "official"
|
||||
PluginOriginUploaded = "uploaded"
|
||||
|
||||
PluginKindProtocol = "protocol"
|
||||
PluginKindApplication = "application"
|
||||
|
||||
PluginScopeSystem = "system"
|
||||
PluginScopeUser = "user"
|
||||
|
||||
PluginConfigurationNone = "none"
|
||||
PluginConfigurationSystem = "system"
|
||||
PluginConfigurationUser = "user"
|
||||
)
|
||||
|
||||
// PluginManagementView is computed by the host. Uploaded manifests cannot
|
||||
// choose their own privilege or activation scope.
|
||||
type PluginManagementView struct {
|
||||
Origin string `json:"origin"`
|
||||
Kind string `json:"kind"`
|
||||
ActivationScope string `json:"activationScope"`
|
||||
ConfigurationScope string `json:"configurationScope"`
|
||||
}
|
||||
|
||||
type PluginStateView struct {
|
||||
PluginID string `json:"pluginId"`
|
||||
PlatformAvailable bool `json:"platformAvailable"`
|
||||
UserEnabled bool `json:"userEnabled"`
|
||||
UserConfigured bool `json:"userConfigured"`
|
||||
EffectiveEnabled bool `json:"effectiveEnabled"`
|
||||
CanToggle bool `json:"canToggle"`
|
||||
CanConfigure bool `json:"canConfigure"`
|
||||
BlockedReason string `json:"blockedReason,omitempty"`
|
||||
}
|
||||
|
||||
type AdminPluginStateView struct {
|
||||
PluginStateView
|
||||
EnabledUserCount int64 `json:"enabledUserCount"`
|
||||
}
|
||||
|
||||
var officialApplicationPolicies = map[string]PluginManagementView{
|
||||
WorkflowPluginRunningHub: {
|
||||
Origin: PluginOriginOfficial, Kind: PluginKindApplication,
|
||||
ActivationScope: PluginScopeUser, ConfigurationScope: PluginConfigurationUser,
|
||||
},
|
||||
WorkflowPluginComfyUI: {
|
||||
Origin: PluginOriginOfficial, Kind: PluginKindApplication,
|
||||
ActivationScope: PluginScopeUser, ConfigurationScope: PluginConfigurationUser,
|
||||
},
|
||||
PluginEagleAssetConnector: {
|
||||
Origin: PluginOriginOfficial, Kind: PluginKindApplication,
|
||||
ActivationScope: PluginScopeUser, ConfigurationScope: PluginConfigurationUser,
|
||||
},
|
||||
PluginPromptOptimizer: {
|
||||
Origin: PluginOriginOfficial, Kind: PluginKindApplication,
|
||||
ActivationScope: PluginScopeUser, ConfigurationScope: PluginConfigurationNone,
|
||||
},
|
||||
PluginPortraitClearance: {
|
||||
Origin: PluginOriginOfficial, Kind: PluginKindApplication,
|
||||
ActivationScope: PluginScopeUser, ConfigurationScope: PluginConfigurationNone,
|
||||
},
|
||||
}
|
||||
|
||||
func pluginManagement(pluginID string, source string) PluginManagementView {
|
||||
if strings.TrimSpace(source) == PluginOriginUploaded {
|
||||
return PluginManagementView{
|
||||
Origin: PluginOriginUploaded, Kind: PluginKindProtocol,
|
||||
ActivationScope: PluginScopeSystem, ConfigurationScope: PluginConfigurationSystem,
|
||||
}
|
||||
}
|
||||
if policy, ok := officialApplicationPolicies[strings.TrimSpace(pluginID)]; ok {
|
||||
return policy
|
||||
}
|
||||
return PluginManagementView{
|
||||
Origin: PluginOriginOfficial, Kind: PluginKindProtocol,
|
||||
ActivationScope: PluginScopeSystem, ConfigurationScope: PluginConfigurationSystem,
|
||||
}
|
||||
}
|
||||
|
||||
func knownPluginIDs(items []PluginView) []string {
|
||||
seen := make(map[string]struct{}, len(items)+len(officialApplicationPolicies))
|
||||
ids := make([]string, 0, len(items)+len(officialApplicationPolicies))
|
||||
for id := range officialApplicationPolicies {
|
||||
seen[id] = struct{}{}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
for _, item := range items {
|
||||
if _, exists := seen[item.Manifest.ID]; exists {
|
||||
continue
|
||||
}
|
||||
seen[item.Manifest.ID] = struct{}{}
|
||||
ids = append(ids, item.Manifest.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func runtimePluginByID(items []PluginView, pluginID string) (PluginView, bool) {
|
||||
for _, item := range items {
|
||||
if item.Manifest.ID == pluginID {
|
||||
return item, true
|
||||
}
|
||||
}
|
||||
return PluginView{}, false
|
||||
}
|
||||
|
||||
func (s *Service) PluginStatesForUser(actor *model.User) (map[string]PluginStateView, error) {
|
||||
items := s.Plugins()
|
||||
result := make(map[string]PluginStateView, len(items)+len(officialApplicationPolicies))
|
||||
for _, pluginID := range knownPluginIDs(items) {
|
||||
state, err := s.pluginStateForUser(actor, pluginID, items)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[pluginID] = state
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Service) pluginStateForUser(actor *model.User, pluginID string, items []PluginView) (PluginStateView, error) {
|
||||
runtimePlugin, hasRuntime := runtimePluginByID(items, pluginID)
|
||||
source := "bundled"
|
||||
if hasRuntime {
|
||||
source = runtimePlugin.Source
|
||||
} else if _, known := officialApplicationPolicies[pluginID]; !known {
|
||||
return PluginStateView{}, fmt.Errorf("插件 %q 不存在", pluginID)
|
||||
}
|
||||
policy := pluginManagement(pluginID, source)
|
||||
platformAvailable := policy.Kind == PluginKindApplication
|
||||
if hasRuntime && (policy.ActivationScope == PluginScopeSystem || s.repo == nil) {
|
||||
platformAvailable = runtimePlugin.Status == "enabled"
|
||||
}
|
||||
if s.repo != nil {
|
||||
platformState, err := s.repo.PluginPlatformState(pluginID)
|
||||
if err != nil {
|
||||
return PluginStateView{}, fmt.Errorf("读取插件平台状态:%w", err)
|
||||
}
|
||||
if platformState != nil {
|
||||
platformAvailable = platformState.Available
|
||||
}
|
||||
}
|
||||
|
||||
userEnabled := false
|
||||
userConfigured := false
|
||||
if policy.ActivationScope == PluginScopeUser && actor != nil {
|
||||
if s.repo != nil {
|
||||
userState, err := s.repo.UserPluginState(actor.ID, pluginID)
|
||||
if err != nil {
|
||||
return PluginStateView{}, fmt.Errorf("读取用户插件状态:%w", err)
|
||||
}
|
||||
if userState != nil {
|
||||
userEnabled, userConfigured = userState.Enabled, true
|
||||
}
|
||||
}
|
||||
// Preserve the old globally-enabled workflow behavior until each user
|
||||
// explicitly saves a personal choice. Other official applications were
|
||||
// already controlled by each user's local installation state.
|
||||
if !userConfigured && hasRuntime && isLegacyWorkflowPlugin(pluginID) {
|
||||
userEnabled = runtimePlugin.Status == "enabled"
|
||||
}
|
||||
}
|
||||
effective := platformAvailable
|
||||
if policy.ActivationScope == PluginScopeUser {
|
||||
effective = platformAvailable && userEnabled
|
||||
}
|
||||
state := PluginStateView{
|
||||
PluginID: pluginID, PlatformAvailable: platformAvailable,
|
||||
UserEnabled: userEnabled, UserConfigured: userConfigured,
|
||||
EffectiveEnabled: effective,
|
||||
CanToggle: policy.ActivationScope == PluginScopeUser && platformAvailable,
|
||||
CanConfigure: policy.ConfigurationScope == PluginConfigurationUser && platformAvailable,
|
||||
}
|
||||
if !platformAvailable {
|
||||
state.BlockedReason = "管理员已停用该插件"
|
||||
} else if policy.ActivationScope == PluginScopeSystem {
|
||||
state.BlockedReason = "系统插件由管理员统一管理"
|
||||
}
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func isLegacyWorkflowPlugin(pluginID string) bool {
|
||||
return pluginID == WorkflowPluginRunningHub || pluginID == WorkflowPluginComfyUI
|
||||
}
|
||||
|
||||
func (s *Service) SetUserPluginEnabled(actor *model.User, pluginID string, enabled bool) (PluginStateView, error) {
|
||||
if actor == nil || strings.TrimSpace(actor.ID) == "" {
|
||||
return PluginStateView{}, Forbidden("请先登录")
|
||||
}
|
||||
items := s.Plugins()
|
||||
current, err := s.pluginStateForUser(actor, pluginID, items)
|
||||
if err != nil {
|
||||
return PluginStateView{}, err
|
||||
}
|
||||
runtimePlugin, hasRuntime := runtimePluginByID(items, pluginID)
|
||||
source := "bundled"
|
||||
if hasRuntime {
|
||||
source = runtimePlugin.Source
|
||||
}
|
||||
if pluginManagement(pluginID, source).ActivationScope != PluginScopeUser {
|
||||
return PluginStateView{}, Forbidden("系统插件只能由管理员统一管理")
|
||||
}
|
||||
if !current.PlatformAvailable {
|
||||
return PluginStateView{}, Forbidden("管理员已停用该插件")
|
||||
}
|
||||
if s.repo == nil {
|
||||
return PluginStateView{}, fmt.Errorf("插件状态存储未初始化")
|
||||
}
|
||||
now := time.Now()
|
||||
state := &model.UserPluginState{ID: newID(), UserID: actor.ID, PluginID: pluginID, Enabled: enabled, CreatedAt: now, UpdatedAt: now}
|
||||
if err := s.repo.SaveUserPluginState(state); err != nil {
|
||||
return PluginStateView{}, fmt.Errorf("保存用户插件状态:%w", err)
|
||||
}
|
||||
return s.pluginStateForUser(actor, pluginID, items)
|
||||
}
|
||||
|
||||
func (s *Service) AdminPluginStates(actor *model.User) (map[string]AdminPluginStateView, error) {
|
||||
if err := s.RequireAdmin(actor); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
states, err := s.PluginStatesForUser(actor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
counts, err := s.repo.EnabledPluginUserCounts()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("统计插件启用用户数:%w", err)
|
||||
}
|
||||
result := make(map[string]AdminPluginStateView, len(states))
|
||||
for pluginID, state := range states {
|
||||
result[pluginID] = AdminPluginStateView{PluginStateView: state, EnabledUserCount: counts[pluginID]}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Service) SetPluginPlatformAvailability(actor *model.User, pluginID string, available bool) (AdminPluginStateView, error) {
|
||||
if err := s.RequireAdmin(actor); err != nil {
|
||||
return AdminPluginStateView{}, err
|
||||
}
|
||||
items := s.Plugins()
|
||||
runtimePlugin, hasRuntime := runtimePluginByID(items, pluginID)
|
||||
source := "bundled"
|
||||
if hasRuntime {
|
||||
source = runtimePlugin.Source
|
||||
} else if _, known := officialApplicationPolicies[pluginID]; !known {
|
||||
return AdminPluginStateView{}, fmt.Errorf("插件 %q 不存在", pluginID)
|
||||
}
|
||||
policy := pluginManagement(pluginID, source)
|
||||
previousRuntimeEnabled := false
|
||||
runtimeChanged := false
|
||||
if policy.ActivationScope == PluginScopeSystem {
|
||||
if !hasRuntime {
|
||||
return AdminPluginStateView{}, fmt.Errorf("插件 %q 缺少运行时", pluginID)
|
||||
}
|
||||
previousRuntimeEnabled = runtimePlugin.Status == "enabled"
|
||||
if _, err := s.SetPluginEnabled(pluginID, available); err != nil {
|
||||
return AdminPluginStateView{}, err
|
||||
}
|
||||
runtimeChanged = previousRuntimeEnabled != available
|
||||
}
|
||||
now := time.Now()
|
||||
platformState := &model.PluginPlatformState{PluginID: pluginID, Available: available, UpdatedBy: actor.ID, CreatedAt: now, UpdatedAt: now}
|
||||
if err := s.repo.SavePluginPlatformState(platformState); err != nil {
|
||||
if runtimeChanged {
|
||||
_, _ = s.SetPluginEnabled(pluginID, previousRuntimeEnabled)
|
||||
}
|
||||
return AdminPluginStateView{}, fmt.Errorf("保存插件平台状态:%w", err)
|
||||
}
|
||||
if err := s.appendAdminAudit(actor, "plugin.availability.update", "plugin", pluginID, "更新插件平台可用状态", map[string]any{"available": available, "kind": policy.Kind, "origin": policy.Origin}); err != nil {
|
||||
return AdminPluginStateView{}, err
|
||||
}
|
||||
states, err := s.AdminPluginStates(actor)
|
||||
if err != nil {
|
||||
return AdminPluginStateView{}, err
|
||||
}
|
||||
return states[pluginID], nil
|
||||
}
|
||||
|
||||
func (s *Service) RequirePluginForUser(userID string, pluginID string) error {
|
||||
state, err := s.pluginStateForUser(&model.User{ID: strings.TrimSpace(userID)}, pluginID, s.Plugins())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !state.EffectiveEnabled {
|
||||
if state.BlockedReason != "" {
|
||||
return Forbidden(state.BlockedReason)
|
||||
}
|
||||
return Forbidden("插件未启用")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
71
backend/internal/service/plugin_management_test.go
Normal file
71
backend/internal/service/plugin_management_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"infinite-canvas/backend/internal/model"
|
||||
"infinite-canvas/backend/internal/repository"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestApplicationPluginUsesUserStateUnderPlatformAvailability(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open("file:"+newID()+"?mode=memory&cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.PluginPlatformState{}, &model.UserPluginState{}, &model.AdminAuditEvent{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
center, err := newPluginRuntime(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
svc := &Service{repo: repository.New(db), pluginRuntime: center}
|
||||
user := &model.User{ID: "user-1", Role: model.UserRoleUser}
|
||||
admin := &model.User{ID: "admin-1", Role: model.UserRoleAdmin}
|
||||
|
||||
states, err := svc.PluginStatesForUser(user)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
initial := states[WorkflowPluginRunningHub]
|
||||
if !initial.PlatformAvailable || initial.UserEnabled || initial.EffectiveEnabled || !initial.CanToggle {
|
||||
t.Fatalf("initial RunningHub state = %#v", initial)
|
||||
}
|
||||
|
||||
enabled, err := svc.SetUserPluginEnabled(user, WorkflowPluginRunningHub, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !enabled.UserConfigured || !enabled.UserEnabled || !enabled.EffectiveEnabled {
|
||||
t.Fatalf("enabled RunningHub state = %#v", enabled)
|
||||
}
|
||||
if _, err := svc.SetPluginPlatformAvailability(user, WorkflowPluginRunningHub, false); err == nil {
|
||||
t.Fatal("ordinary user changed platform plugin availability")
|
||||
}
|
||||
disabled, err := svc.SetPluginPlatformAvailability(admin, WorkflowPluginRunningHub, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if disabled.PlatformAvailable || disabled.EffectiveEnabled || disabled.EnabledUserCount != 1 {
|
||||
t.Fatalf("platform-disabled RunningHub state = %#v", disabled)
|
||||
}
|
||||
if err := svc.RequireWorkflowPluginForUser(user.ID, "runninghub-workflow-image"); err == nil {
|
||||
t.Fatal("platform-disabled workflow was accepted for a new user request")
|
||||
}
|
||||
if _, err := svc.SetPluginPlatformAvailability(admin, WorkflowPluginRunningHub, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := svc.RequireWorkflowPluginForUser(user.ID, "runninghub-workflow-video"); err != nil {
|
||||
t.Fatalf("restored user workflow rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadedManifestCannotClaimUserActivationScope(t *testing.T) {
|
||||
policy := pluginManagement(PluginPromptOptimizer, PluginOriginUploaded)
|
||||
if policy.Origin != PluginOriginUploaded || policy.ActivationScope != PluginScopeSystem || policy.ConfigurationScope != PluginConfigurationSystem {
|
||||
t.Fatalf("uploaded plugin policy = %#v", policy)
|
||||
}
|
||||
}
|
||||
@@ -23,15 +23,16 @@ const protocolPluginMaxBytes = protocol.PluginManifestMaxBytes
|
||||
// plugin center. Protocol-specific runtime data is nested under Protocol so
|
||||
// the public plugin contract can grow without adding another center API.
|
||||
type PluginView struct {
|
||||
Manifest PluginManifestView `json:"manifest"`
|
||||
Source string `json:"source"`
|
||||
FileName string `json:"fileName"`
|
||||
Package string `json:"package"`
|
||||
SHA256 string `json:"sha256"`
|
||||
InstalledAt time.Time `json:"installedAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Manifest PluginManifestView `json:"manifest"`
|
||||
Source string `json:"source"`
|
||||
FileName string `json:"fileName"`
|
||||
Package string `json:"package"`
|
||||
SHA256 string `json:"sha256"`
|
||||
InstalledAt time.Time `json:"installedAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Management PluginManagementView `json:"management"`
|
||||
}
|
||||
|
||||
type PluginManifestView struct {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"infinite-canvas/backend/internal/model"
|
||||
"infinite-canvas/backend/internal/protocol"
|
||||
@@ -168,7 +169,11 @@ func (s *Service) Plugins() []PluginView {
|
||||
if s.pluginRuntime == nil {
|
||||
return []PluginView{}
|
||||
}
|
||||
return s.pluginRuntime.list()
|
||||
items := s.pluginRuntime.list()
|
||||
for index := range items {
|
||||
items[index].Management = pluginManagement(items[index].Manifest.ID, items[index].Source)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// PluginsForUser keeps the plugin center response aligned with the public
|
||||
@@ -188,7 +193,7 @@ func (s *Service) PluginsForUser(actor *model.User) ([]PluginView, error) {
|
||||
}
|
||||
filtered := make([]PluginView, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.Source != "bundled" {
|
||||
if item.Management.ActivationScope == PluginScopeUser {
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
}
|
||||
@@ -202,6 +207,33 @@ func (s *Service) InstallPlugin(data []byte, fileName string) (PluginView, error
|
||||
return s.pluginRuntime.install(data, fileName)
|
||||
}
|
||||
|
||||
func (s *Service) InstallPluginForAdmin(actor *model.User, data []byte, fileName string) (PluginView, error) {
|
||||
if err := s.RequireAdmin(actor); err != nil {
|
||||
return PluginView{}, err
|
||||
}
|
||||
parsed, err := protocol.ParsePluginPackage(data)
|
||||
if err != nil {
|
||||
return PluginView{}, err
|
||||
}
|
||||
if _, reserved := officialApplicationPolicies[parsed.Manifest.Metadata.ID]; reserved {
|
||||
return PluginView{}, fmt.Errorf("插件 ID %q 由官方应用保留", parsed.Manifest.Metadata.ID)
|
||||
}
|
||||
plugin, err := s.InstallPlugin(data, fileName)
|
||||
if err != nil {
|
||||
return PluginView{}, err
|
||||
}
|
||||
plugin.Management = pluginManagement(plugin.Manifest.ID, plugin.Source)
|
||||
now := time.Now()
|
||||
state := &model.PluginPlatformState{PluginID: plugin.Manifest.ID, Available: plugin.Status == "enabled", UpdatedBy: actor.ID, CreatedAt: now, UpdatedAt: now}
|
||||
if err := s.repo.SavePluginPlatformState(state); err != nil {
|
||||
return PluginView{}, fmt.Errorf("保存插件平台状态:%w", err)
|
||||
}
|
||||
if err := s.appendAdminAudit(actor, "plugin.install", "plugin", plugin.Manifest.ID, "安装自定义插件", map[string]any{"fileName": fileName, "sha256": plugin.SHA256}); err != nil {
|
||||
return PluginView{}, err
|
||||
}
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
func (s *Service) PluginPackage(id string) ([]byte, string, error) {
|
||||
if s.pluginRuntime == nil {
|
||||
return nil, "", fmt.Errorf("插件运行时未初始化")
|
||||
@@ -235,3 +267,19 @@ func (s *Service) UninstallPlugin(id string) error {
|
||||
}
|
||||
return s.pluginRuntime.uninstall(id)
|
||||
}
|
||||
|
||||
func (s *Service) UninstallPluginForAdmin(actor *model.User, id string) error {
|
||||
if err := s.RequireAdmin(actor); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.UninstallPlugin(id); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.repo.DeleteUserPluginStates(id); err != nil {
|
||||
return fmt.Errorf("清理用户插件状态:%w", err)
|
||||
}
|
||||
if err := s.repo.DeletePluginPlatformState(id); err != nil {
|
||||
return fmt.Errorf("清理插件平台状态:%w", err)
|
||||
}
|
||||
return s.appendAdminAudit(actor, "plugin.uninstall", "plugin", id, "卸载自定义插件", nil)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func (s *Service) CreateTask(userID string, req CreateTaskRequest) (*model.Task,
|
||||
frontendEnabled := false
|
||||
if workflowProviderTask {
|
||||
config, _ := normalizedInput["config"].(map[string]any)
|
||||
if err := s.RequireWorkflowPluginForInterface(strings.TrimSpace(fmt.Sprint(config["interfaceType"]))); err != nil {
|
||||
if err := s.RequireWorkflowPluginForUser(userID, strings.TrimSpace(fmt.Sprint(config["interfaceType"]))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"infinite-canvas/backend/internal/model"
|
||||
"infinite-canvas/backend/internal/protocol"
|
||||
)
|
||||
|
||||
@@ -66,14 +67,34 @@ func workflowPluginIDForInterface(value string) (string, bool) {
|
||||
|
||||
func (s *Service) WorkflowPluginStatuses() map[string]string {
|
||||
statuses := make(map[string]string, 2)
|
||||
for _, plugin := range s.Plugins() {
|
||||
if plugin.Manifest.ID == WorkflowPluginRunningHub || plugin.Manifest.ID == WorkflowPluginComfyUI {
|
||||
statuses[plugin.Manifest.ID] = plugin.Status
|
||||
for _, pluginID := range []string{WorkflowPluginRunningHub, WorkflowPluginComfyUI} {
|
||||
state, err := s.pluginStateForUser(nil, pluginID, s.Plugins())
|
||||
if err == nil && state.PlatformAvailable {
|
||||
statuses[pluginID] = "enabled"
|
||||
} else {
|
||||
statuses[pluginID] = "disabled"
|
||||
}
|
||||
}
|
||||
return statuses
|
||||
}
|
||||
|
||||
func (s *Service) WorkflowPluginStatusesForUser(userID string) (map[string]string, error) {
|
||||
statuses := make(map[string]string, 2)
|
||||
actor := &model.User{ID: strings.TrimSpace(userID)}
|
||||
for _, pluginID := range []string{WorkflowPluginRunningHub, WorkflowPluginComfyUI} {
|
||||
state, err := s.pluginStateForUser(actor, pluginID, s.Plugins())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if state.EffectiveEnabled {
|
||||
statuses[pluginID] = "enabled"
|
||||
} else {
|
||||
statuses[pluginID] = "disabled"
|
||||
}
|
||||
}
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
func (s *Service) RequireWorkflowPluginForInterface(interfaceType string) error {
|
||||
pluginID, ok := workflowPluginIDForInterface(normalizeWorkflowInterfaceType(interfaceType))
|
||||
if !ok {
|
||||
@@ -89,6 +110,20 @@ func (s *Service) RequireWorkflowPluginForInterface(interfaceType string) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) RequireWorkflowPluginForUser(userID string, interfaceType string) error {
|
||||
pluginID, ok := workflowPluginIDForInterface(normalizeWorkflowInterfaceType(interfaceType))
|
||||
if !ok {
|
||||
return Forbidden("未知工作流插件")
|
||||
}
|
||||
if err := s.RequirePluginForUser(userID, pluginID); err != nil {
|
||||
if pluginID == WorkflowPluginRunningHub {
|
||||
return Forbidden("RunningHub 工作流插件未启用")
|
||||
}
|
||||
return Forbidden("ComfyUI Bridge 工作流插件未启用")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeWorkflowInterfaceType(value string) string {
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
return value
|
||||
|
||||
Reference in New Issue
Block a user