From a32764e2ea0ae157b99d98250682d25f91623af3 Mon Sep 17 00:00:00 2001 From: hjlarry Date: Wed, 3 Jun 2026 16:32:08 +0800 Subject: [PATCH 1/2] feat: add ListPluginsByCategory --- internal/server/controllers/plugins.go | 11 +++ internal/server/http_server.go | 1 + internal/service/manage_plugin.go | 129 +++++++++++++++++++++++++ 3 files changed, 141 insertions(+) diff --git a/internal/server/controllers/plugins.go b/internal/server/controllers/plugins.go index a28b8393d..cdbd03236 100644 --- a/internal/server/controllers/plugins.go +++ b/internal/server/controllers/plugins.go @@ -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"` + 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"` diff --git a/internal/server/http_server.go b/internal/server/http_server.go index 9e197f4b4..1187e15f9 100644 --- a/internal/server/http_server.go +++ b/internal/server/http_server.go @@ -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) diff --git a/internal/service/manage_plugin.go b/internal/service/manage_plugin.go index 5933950f1..230c0b4a8 100644 --- a/internal/service/manage_plugin.go +++ b/internal/service/manage_plugin.go @@ -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"` +} + +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"` @@ -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 + 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() + } + + 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 { From bf16b5264e38ebaedbb8f14a610c63d20c853e28 Mon Sep 17 00:00:00 2001 From: hjlarry Date: Wed, 3 Jun 2026 16:47:49 +0800 Subject: [PATCH 2/2] feat: install plugin api add optional current task response --- internal/service/install_plugin.go | 7 ++- internal/service/install_plugin_test.go | 74 ++++++++++++++++++++----- internal/tasks/install_plugin_utils.go | 14 ++++- 3 files changed, 77 insertions(+), 18 deletions(-) diff --git a/internal/service/install_plugin.go b/internal/service/install_plugin.go index 0dfdebded..b95deafe2 100644 --- a/internal/service/install_plugin.go +++ b/internal/service/install_plugin.go @@ -27,8 +27,9 @@ import ( ) type InstallPluginResponse struct { - AllInstalled bool `json:"all_installed"` - TaskID string `json:"task_id"` + AllInstalled bool `json:"all_installed"` + TaskID string `json:"task_id"` + Task *models.InstallTask `json:"task,omitempty"` } // Dify supports install multiple plugins to a tenant at once @@ -142,6 +143,7 @@ func InstallMultiplePluginsToTenant( // EE edition reference task should not be the first one // here we use `PrimaryID` to present the user-facing task id TaskID: taskRegistry.PrimaryID(), + Task: taskRegistry.PrimaryTask(), }) } @@ -318,6 +320,7 @@ func UpgradePlugin( return entities.NewSuccessResponse(&InstallPluginResponse{ AllInstalled: false, TaskID: taskRegistry.PrimaryID(), + Task: taskRegistry.PrimaryTask(), }) } diff --git a/internal/service/install_plugin_test.go b/internal/service/install_plugin_test.go index a329c7bf1..298f9f2c4 100644 --- a/internal/service/install_plugin_test.go +++ b/internal/service/install_plugin_test.go @@ -5,9 +5,57 @@ import ( "testing" "github.com/langgenius/dify-plugin-daemon/internal/types/app" + "github.com/langgenius/dify-plugin-daemon/internal/types/models" "github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities" ) +func TestInstallPluginResponseIncludesPrimaryTask(t *testing.T) { + setupTestDB(t) + + identifier, err := plugin_entities.NewPluginUniqueIdentifier("author/test-plugin:1.0.0@abcdef1234567890abcdef1234567890ab") + if err != nil { + t.Fatalf("failed to create plugin unique identifier: %v", err) + } + + taskRegistry, err := createInstallTasks([]string{"tenant-123"}, []models.InstallTaskPluginStatus{ + { + PluginUniqueIdentifier: identifier, + PluginID: identifier.PluginID(), + Status: models.InstallTaskStatusPending, + Source: "marketplace", + }, + }) + if err != nil { + t.Fatalf("failed to create install task: %v", err) + } + + response := InstallPluginResponse{ + AllInstalled: false, + TaskID: taskRegistry.PrimaryID(), + Task: taskRegistry.PrimaryTask(), + } + + data, err := json.Marshal(response) + if err != nil { + t.Fatalf("failed to marshal response: %v", err) + } + + var decoded InstallPluginResponse + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + + if decoded.Task == nil { + t.Fatal("expected response task to be present") + } + if decoded.TaskID == "" { + t.Fatal("expected response task_id to be present") + } + if decoded.Task.ID != decoded.TaskID { + t.Fatalf("task id mismatch: got %s, want %s", decoded.Task.ID, decoded.TaskID) + } +} + func TestUpgradePlugin(t *testing.T) { originalIdentifier, err := plugin_entities.NewPluginUniqueIdentifier("author/test-plugin:1.0.0@abcdef1234567890abcdef1234567890ab") if err != nil { @@ -24,23 +72,23 @@ func TestUpgradePlugin(t *testing.T) { } tests := []struct { - name string - tenantId string - source string - meta map[string]any - originalPluginUniqueIdentifier plugin_entities.PluginUniqueIdentifier - newPluginUniqueIdentifier plugin_entities.PluginUniqueIdentifier - wantSuccess bool - wantAllInstalled bool - wantTaskIDEmpty bool + name string + tenantId string + source string + meta map[string]any + originalPluginUniqueIdentifier plugin_entities.PluginUniqueIdentifier + newPluginUniqueIdentifier plugin_entities.PluginUniqueIdentifier + wantSuccess bool + wantAllInstalled bool + wantTaskIDEmpty bool }{ { name: "same plugin identifiers", tenantId: "tenant-123", source: "test", meta: map[string]any{}, - originalPluginUniqueIdentifier: originalIdentifier, - newPluginUniqueIdentifier: originalIdentifier, + originalPluginUniqueIdentifier: originalIdentifier, + newPluginUniqueIdentifier: originalIdentifier, wantSuccess: false, }, { @@ -48,8 +96,8 @@ func TestUpgradePlugin(t *testing.T) { tenantId: "tenant-123", source: "test", meta: map[string]any{}, - originalPluginUniqueIdentifier: originalIdentifier, - newPluginUniqueIdentifier: newIdentifier, + originalPluginUniqueIdentifier: originalIdentifier, + newPluginUniqueIdentifier: newIdentifier, wantSuccess: false, }, } diff --git a/internal/tasks/install_plugin_utils.go b/internal/tasks/install_plugin_utils.go index c7a4fe6e4..6bee72b36 100644 --- a/internal/tasks/install_plugin_utils.go +++ b/internal/tasks/install_plugin_utils.go @@ -29,13 +29,21 @@ func (r *InstallTaskRegistry) IDs() []string { } func (r *InstallTaskRegistry) PrimaryID() string { - if len(r.Order) == 0 { + task := r.PrimaryTask() + if task == nil { return "" } + return task.ID +} + +func (r *InstallTaskRegistry) PrimaryTask() *models.InstallTask { + if len(r.Order) == 0 { + return nil + } if task, ok := r.Tasks[r.Order[0]]; ok { - return task.ID + return task } - return "" + return nil } func truncateMessage(message string) string {