浏览代码

feat: support upload bundle

Yeuoly 8 月之前
父节点
当前提交
44a2de1f62

+ 3 - 0
internal/core/bundle_packager/bundle_packager.go

@@ -35,4 +35,7 @@ type BundlePackager interface {
 
 	// BumpVersion bumps the version of the bundle
 	BumpVersion(targetVersion manifest_entities.Version)
+
+	// FetchAsset fetches the asset of the bundle
+	FetchAsset(path string) ([]byte, error)
 }

+ 9 - 0
internal/core/bundle_packager/generic.go

@@ -146,3 +146,12 @@ func (p *GenericBundlePackager) Remove(index int) error {
 func (p *GenericBundlePackager) BumpVersion(target manifest_entities.Version) {
 	p.bundle.Version = target
 }
+
+func (p *GenericBundlePackager) FetchAsset(path string) ([]byte, error) {
+	asset, ok := p.assets[path]
+	if !ok {
+		return nil, errors.New("asset not found")
+	}
+
+	return asset.Bytes(), nil
+}

+ 26 - 1
internal/server/controllers/plugins.go

@@ -51,7 +51,32 @@ func UploadPlugin(app *app.Config) gin.HandlerFunc {
 		}
 		defer difyPkgFile.Close()
 
-		c.JSON(http.StatusOK, service.UploadPluginFromPkg(app, c, tenantId, difyPkgFile, verifySignature))
+		c.JSON(http.StatusOK, service.UploadPluginPkg(app, c, tenantId, difyPkgFile, verifySignature))
+	}
+}
+
+func UploadBundle(app *app.Config) gin.HandlerFunc {
+	return func(c *gin.Context) {
+		difyBundleFileHeader, err := c.FormFile("dify_bundle")
+		if err != nil {
+			c.JSON(http.StatusOK, entities.NewErrorResponse(-400, err.Error()))
+			return
+		}
+
+		tenantId := c.Param("tenant_id")
+		if tenantId == "" {
+			c.JSON(http.StatusOK, entities.NewErrorResponse(-400, "Tenant ID is required"))
+			return
+		}
+
+		if difyBundleFileHeader.Size > app.MaxBundlePackageSize {
+			c.JSON(http.StatusOK, entities.NewErrorResponse(-413, "File size exceeds the maximum limit"))
+			return
+		}
+
+		verifySignature := c.PostForm("verify_signature") == "true"
+
+		c.JSON(http.StatusOK, service.UploadPluginBundle(app, c, tenantId, difyBundleFileHeader, verifySignature))
 	}
 }
 

+ 2 - 1
internal/server/http_server.go

@@ -113,7 +113,8 @@ func (app *App) endpointManagementGroup(group *gin.RouterGroup) {
 }
 
 func (app *App) pluginManagementGroup(group *gin.RouterGroup, config *app.Config) {
-	group.POST("/install/upload", controllers.UploadPlugin(config))
+	group.POST("/install/upload/pkg", controllers.UploadPlugin(config))
+	group.POST("/install/upload/bundle", controllers.UploadBundle(config))
 	group.POST("/install/identifiers", controllers.InstallPluginFromIdentifiers(config))
 	group.POST("/install/upgrade", controllers.UpgradePlugin(config))
 	group.GET("/install/tasks/:id", controllers.FetchPluginInstallationTask)

+ 1 - 0
internal/service/manage_plugin.go

@@ -119,6 +119,7 @@ func FetchMissingPluginInstallations(tenant_id string, plugin_unique_identifiers
 				},
 			),
 		),
+		db.Page(1, 256), // TODO: pagination
 	)
 
 	if err != nil {

+ 91 - 1
internal/service/plugin_decoder.go

@@ -6,15 +6,17 @@ import (
 	"mime/multipart"
 
 	"github.com/gin-gonic/gin"
+	"github.com/langgenius/dify-plugin-daemon/internal/core/bundle_packager"
 	"github.com/langgenius/dify-plugin-daemon/internal/core/plugin_manager"
 	"github.com/langgenius/dify-plugin-daemon/internal/core/plugin_packager/decoder"
 	"github.com/langgenius/dify-plugin-daemon/internal/types/app"
 	"github.com/langgenius/dify-plugin-daemon/internal/types/entities"
+	"github.com/langgenius/dify-plugin-daemon/internal/types/entities/bundle_entities"
 	"github.com/langgenius/dify-plugin-daemon/internal/types/entities/plugin_entities"
 	"github.com/langgenius/dify-plugin-daemon/internal/utils/cache"
 )
 
-func UploadPluginFromPkg(
+func UploadPluginPkg(
 	config *app.Config,
 	c *gin.Context,
 	tenant_id string,
@@ -56,6 +58,94 @@ func UploadPluginFromPkg(
 	})
 }
 
+func UploadPluginBundle(
+	config *app.Config,
+	c *gin.Context,
+	tenant_id string,
+	dify_bundle_file *multipart.FileHeader,
+	verify_signature bool,
+) *entities.Response {
+	packager, err := bundle_packager.NewZipBundlePackager(dify_bundle_file.Filename)
+	if err != nil {
+		return entities.NewErrorResponse(-500, errors.Join(err, errors.New("failed to create bundle packager")).Error())
+	}
+
+	// load bundle
+	bundle, err := packager.Manifest()
+	if err != nil {
+		return entities.NewErrorResponse(-500, errors.Join(err, errors.New("failed to load bundle manifest")).Error())
+	}
+
+	manager := plugin_manager.Manager()
+
+	result := []map[string]any{}
+
+	for _, dependency := range bundle.Dependencies {
+		if dependency.Type == bundle_entities.DEPENDENCY_TYPE_GITHUB {
+			if dep, ok := dependency.Value.(bundle_entities.GithubDependency); ok {
+				result = append(result, map[string]any{
+					"type": "github",
+					"value": map[string]any{
+						"repo_address": dep.RepoPattern.Repo(),
+						"github_repo":  dep.RepoPattern.GithubRepo(),
+						"release":      dep.RepoPattern.Release(),
+						"packages":     dep.RepoPattern.Asset(),
+					},
+				})
+			} else if dep, ok := dependency.Value.(bundle_entities.MarketplaceDependency); ok {
+				result = append(result, map[string]any{
+					"type": "marketplace",
+					"value": map[string]any{
+						"organization": dep.MarketplacePattern.Organization(),
+						"plugin":       dep.MarketplacePattern.Plugin(),
+						"version":      dep.MarketplacePattern.Version(),
+					},
+				})
+			} else if dep, ok := dependency.Value.(bundle_entities.PackageDependency); ok {
+				// fetch package
+				path := dep.Path
+				if asset, err := packager.FetchAsset(path); err != nil {
+					return entities.NewErrorResponse(-500, errors.Join(err, errors.New("failed to fetch package")).Error())
+				} else {
+					// decode and save
+					decoder, err := decoder.NewZipPluginDecoder(asset)
+					if err != nil {
+						return entities.NewErrorResponse(-500, err.Error())
+					}
+
+					pluginUniqueIdentifier, err := decoder.UniqueIdentity()
+					if err != nil {
+						return entities.NewErrorResponse(-500, err.Error())
+					}
+
+					declaration, err := manager.SavePackage(pluginUniqueIdentifier, asset)
+					if err != nil {
+						return entities.NewErrorResponse(-500, err.Error())
+					}
+
+					if config.ForceVerifyingSignature || verify_signature {
+						if !declaration.Verified {
+							return entities.NewErrorResponse(-500, errors.Join(err, errors.New(
+								"plugin verification has been enabled, and the plugin you want to install has a bad signature",
+							)).Error())
+						}
+					}
+
+					result = append(result, map[string]any{
+						"type": "package",
+						"value": map[string]any{
+							"unique_identifier": pluginUniqueIdentifier,
+							"manifest":          declaration,
+						},
+					})
+				}
+			}
+		}
+	}
+
+	return entities.NewSuccessResponse(result)
+}
+
 func FetchPluginManifest(
 	tenant_id string,
 	pluginUniqueIdentifier plugin_entities.PluginUniqueIdentifier,

+ 3 - 3
internal/types/app/config.go

@@ -77,9 +77,9 @@ type Config struct {
 	DifyPluginServerlessConnectorURL    *string `envconfig:"DIFY_PLUGIN_SERVERLESS_CONNECTOR_URL"`
 	DifyPluginServerlessConnectorAPIKey *string `envconfig:"DIFY_PLUGIN_SERVERLESS_CONNECTOR_API_KEY"`
 
-	MaxPluginPackageSize int64 `envconfig:"MAX_PLUGIN_PACKAGE_SIZE" validate:"required"`
-
-	MaxAWSLambdaTransactionTimeout int `envconfig:"MAX_AWS_LAMBDA_TRANSACTION_TIMEOUT"`
+	MaxPluginPackageSize           int64 `envconfig:"MAX_PLUGIN_PACKAGE_SIZE" validate:"required"`
+	MaxBundlePackageSize           int64 `envconfig:"MAX_BUNDLE_PACKAGE_SIZE" validate:"required"`
+	MaxAWSLambdaTransactionTimeout int   `envconfig:"MAX_AWS_LAMBDA_TRANSACTION_TIMEOUT"`
 
 	PythonInterpreterPath string `envconfig:"PYTHON_INTERPRETER_PATH"`
 }

+ 1 - 0
internal/types/app/default.go

@@ -12,6 +12,7 @@ func (config *Config) SetDefault() {
 	setDefaultInt(&config.PluginRemoteInstallServerEventLoopNums, 8)
 	setDefaultInt(&config.PluginRemoteInstallingMaxConn, 256)
 	setDefaultInt(&config.MaxPluginPackageSize, 52428800)
+	setDefaultInt(&config.MaxBundlePackageSize, 52428800*12)
 	setDefaultInt(&config.MaxAWSLambdaTransactionTimeout, 150)
 	setDefaultInt(&config.PluginMaxExecutionTimeout, 240)
 	setDefaultString(&config.PluginStorageType, "local")

+ 9 - 0
internal/types/entities/bundle_entities/dependency.go

@@ -87,6 +87,15 @@ func (p GithubRepoPattern) Repo() string {
 	return fmt.Sprintf("https://github.com/%s/%s", organization, repo)
 }
 
+func (p GithubRepoPattern) GithubRepo() string {
+	split := p.Split()
+	if len(split) < 3 {
+		return ""
+	}
+
+	return fmt.Sprintf("%s/%s", split[0], split[1])
+}
+
 func (p GithubRepoPattern) Release() string {
 	split := p.Split()
 	if len(split) < 3 {