Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions internal/server/controllers/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,17 @@ func ListPlugins(c *gin.Context) {
})
}

func ListPluginsByCategory(c *gin.Context) {
BindRequest(c, func(request struct {
TenantID string `uri:"tenant_id" validate:"required"`
Category plugin_entities.PluginCategory `uri:"category" validate:"required"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Instead of validating the category manually in the service layer, you can leverage the oneof validator tag in the binding struct. This ensures that invalid categories are rejected immediately at the controller level.

Category plugin_entities.PluginCategory `uri:"category" validate:"required,oneof=tool model extension agent-strategy datasource trigger"`

Page int `form:"page" validate:"required,min=1"`
PageSize int `form:"page_size" validate:"required,min=1,max=256"`
}) {
c.JSON(http.StatusOK, service.ListPluginsByCategory(request.TenantID, request.Category, request.Page, request.PageSize))
})
}

func BatchFetchPluginInstallationByIDs(c *gin.Context) {
BindRequest(c, func(request struct {
TenantID string `uri:"tenant_id" validate:"required"`
Expand Down
1 change: 1 addition & 0 deletions internal/server/http_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ func (app *App) pluginManagementGroup(group *gin.RouterGroup, config *app.Config
group.GET("/fetch/readme", controllers.FetchPluginReadme)
group.POST("/uninstall", controllers.UninstallPlugin)
group.GET("/list", controllers.ListPlugins)
group.GET("/:category/list", controllers.ListPluginsByCategory)
group.POST("/installation/fetch/batch", controllers.BatchFetchPluginInstallationByIDs)
group.POST("/installation/missing", controllers.FetchMissingPluginInstallations)
group.GET("/models", controllers.ListModels)
Expand Down
129 changes: 129 additions & 0 deletions internal/service/manage_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,46 @@ import (
"github.com/langgenius/dify-plugin-daemon/pkg/utils/strings"
)

const pluginCategoryListScanPageSize = 256

type pluginInstallationResponse struct {
ID string `json:"id"`
Name string `json:"name"`
PluginID string `json:"plugin_id"`
TenantID string `json:"tenant_id"`
PluginUniqueIdentifier string `json:"plugin_unique_identifier"`
EndpointsActive int `json:"endpoints_active"`
EndpointsSetups int `json:"endpoints_setups"`
InstallationID string `json:"installation_id"`
Declaration *plugin_entities.PluginDeclaration `json:"declaration"`
RuntimeType plugin_entities.PluginRuntimeType `json:"runtime_type"`
Version manifest_entities.Version `json:"version"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Source string `json:"source"`
Checksum string `json:"checksum"`
Meta map[string]any `json:"meta"`
}
Comment on lines +20 to +37

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The pluginInstallationResponse struct is identical to the installation struct defined locally inside ListPlugins (line 59). To improve maintainability and eliminate code duplication, consider refactoring ListPlugins to reuse this package-level struct.


type pluginListResponse struct {
List []pluginInstallationResponse `json:"list"`
HasMore bool `json:"has_more"`
}

func isValidPluginCategory(category plugin_entities.PluginCategory) bool {
switch category {
case plugin_entities.PLUGIN_CATEGORY_TOOL,
plugin_entities.PLUGIN_CATEGORY_MODEL,
plugin_entities.PLUGIN_CATEGORY_EXTENSION,
plugin_entities.PLUGIN_CATEGORY_AGENT_STRATEGY,
plugin_entities.PLUGIN_CATEGORY_DATASOURCE,
plugin_entities.PLUGIN_CATEGORY_TRIGGER:
return true
default:
return false
}
}

func ListPlugins(tenant_id string, page int, page_size int) *entities.Response {
type installation struct {
ID string `json:"id"`
Expand Down Expand Up @@ -105,6 +145,95 @@ func ListPlugins(tenant_id string, page int, page_size int) *entities.Response {
return entities.NewSuccessResponse(finalData)
}

func ListPluginsByCategory(
tenant_id string,
category plugin_entities.PluginCategory,
page int,
page_size int,
) *entities.Response {
if !isValidPluginCategory(category) {
return exception.BadRequestError(errors.New("invalid plugin category")).ToResponse()
}

skippedMatches := (page - 1) * page_size

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If page is less than 1, skippedMatches can result in a negative value. Although the controller validates page >= 1, it is safer to guard against this in the service layer to prevent potential negative index issues if called from other contexts.

Suggested change
skippedMatches := (page - 1) * page_size
skippedMatches := 0
if page > 1 {
skippedMatches = (page - 1) * page_size
}

targetMatches := page_size + 1
data := make([]pluginInstallationResponse, 0, targetMatches)

for scanPage := 1; len(data) < targetMatches; scanPage++ {
pluginInstallations, err := db.GetAll[models.PluginInstallation](
db.Equal("tenant_id", tenant_id),
db.OrderBy("created_at", true),
db.Page(scanPage, pluginCategoryListScanPageSize),
)
if err != nil {
return exception.InternalServerError(err).ToResponse()
}

if len(pluginInstallations) == 0 {
break
}

for _, plugin_installation := range pluginInstallations {
pluginUniqueIdentifier, err := plugin_entities.NewPluginUniqueIdentifier(
plugin_installation.PluginUniqueIdentifier,
)
if err != nil {
return exception.UniqueIdentifierError(err).ToResponse()
}

pluginDeclaration, err := helper.CombinedGetPluginDeclaration(
pluginUniqueIdentifier,
plugin_entities.PluginRuntimeType(plugin_installation.RuntimeType),
)
if err != nil {
return exception.InternalServerError(err).ToResponse()
}
Comment on lines +188 to +190

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If helper.CombinedGetPluginDeclaration fails for any single plugin installation (e.g., due to corrupted files or missing cache), the entire ListPluginsByCategory API will fail with a 500 Internal Server Error. To make the API more robust, consider skipping the corrupted plugin so that the rest of the tenant's plugins can still be loaded successfully.

Suggested change
if err != nil {
return exception.InternalServerError(err).ToResponse()
}
if err != nil {
continue
}


if pluginDeclaration.Category() != category {
continue
}

if skippedMatches > 0 {
skippedMatches--
continue
}

data = append(data, pluginInstallationResponse{
ID: plugin_installation.ID,
Name: pluginDeclaration.Name,
TenantID: plugin_installation.TenantID,
PluginID: pluginUniqueIdentifier.PluginID(),
PluginUniqueIdentifier: pluginUniqueIdentifier.String(),
InstallationID: plugin_installation.ID,
Declaration: pluginDeclaration,
EndpointsSetups: plugin_installation.EndpointsSetups,
EndpointsActive: plugin_installation.EndpointsActive,
RuntimeType: plugin_entities.PluginRuntimeType(plugin_installation.RuntimeType),
Version: pluginDeclaration.Version,
CreatedAt: plugin_installation.CreatedAt,
UpdatedAt: plugin_installation.UpdatedAt,
Source: plugin_installation.Source,
Meta: plugin_installation.Meta,
Checksum: pluginUniqueIdentifier.Checksum(),
})
if len(data) == targetMatches {
break
}
}

if len(pluginInstallations) < pluginCategoryListScanPageSize {
break
}
}

hasMore := len(data) > page_size
if hasMore {
data = data[:page_size]
}

return entities.NewSuccessResponse(pluginListResponse{List: data, HasMore: hasMore})
}

// Using plugin_ids to fetch plugin installations
func BatchFetchPluginInstallationByIDs(tenant_id string, plugin_ids []string) *entities.Response {
type installation struct {
Expand Down
Loading