Files
2025-12-02 16:12:17 +08:00

225 lines
6.6 KiB
Go

package plugin_manager
import (
"fmt"
"strings"
lru "github.com/hashicorp/golang-lru/v2"
"github.com/langgenius/dify-cloud-kit/oss"
controlpanel "github.com/langgenius/dify-plugin-daemon/internal/core/control_panel"
"github.com/langgenius/dify-plugin-daemon/internal/core/dify_invocation"
"github.com/langgenius/dify-plugin-daemon/internal/core/dify_invocation/calldify"
"github.com/langgenius/dify-plugin-daemon/internal/core/plugin_manager/media_transport"
serverless "github.com/langgenius/dify-plugin-daemon/internal/core/serverless_connector"
"github.com/langgenius/dify-plugin-daemon/internal/service/install_service"
"github.com/langgenius/dify-plugin-daemon/internal/types/app"
"github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities"
"github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/decoder"
"github.com/langgenius/dify-plugin-daemon/pkg/utils/cache"
"github.com/langgenius/dify-plugin-daemon/pkg/utils/log"
)
type PluginManager struct {
// mediaBucket is used to manage media files like plugin icons, images, etc.
mediaBucket *media_transport.MediaBucket
// packageBucket can be considered as a collection of all uploaded plugin
// original packages, once a package was accepted by Dify,
// it should be stored here.
packageBucket *media_transport.PackageBucket
// installedBucket is used to manage installed plugins, all the installed plugins will be saved here
// `accepted` dose not means `installed`, a installed plugin
// will be scheduled by daemon, daemon copied and move the package
// from `packageBucket` to `installedBucket`
// the copy processing marks a plugin as `installed`
// as for `scheduling`, it's automatically done by control panel
// of course you may use `controlPanel.LaunchLocalPlugin` to start it manually
installedBucket *media_transport.InstalledBucket
// backwardsInvocation is a handle to invoke dify
backwardsInvocation dify_invocation.BackwardsInvocation
config *app.Config
// plugin lifecycle controller
//
// whatever it's local mode or serverless mode, all the signals and calls
// which related to plugin lifecycle should be handled by it.
// so that we can decouple lifetime control and thirdparty service like package management
controlPanel *controlpanel.ControlPanel
}
var (
manager *PluginManager
)
func InitGlobalManager(oss oss.OSS, config *app.Config) *PluginManager {
mediaBucket := media_transport.NewAssetsBucket(
oss,
config.PluginMediaCachePath,
config.PluginMediaCacheSize,
)
installedBucket := media_transport.NewInstalledBucket(
oss,
config.PluginInstalledPath,
)
packageBucket := media_transport.NewPackageBucket(
oss,
config.PluginPackageCachePath,
)
manager = &PluginManager{
mediaBucket: mediaBucket,
packageBucket: packageBucket,
installedBucket: installedBucket,
controlPanel: controlpanel.NewControlPanel(
config,
mediaBucket,
packageBucket,
installedBucket,
),
config: config,
}
// mount control panel notifiers
manager.controlPanel.AddNotifier(&controlpanel.StandardLogger{})
manager.controlPanel.AddNotifier(&install_service.InstallListener{})
return manager
}
func Manager() *PluginManager {
return manager
}
func (p *PluginManager) GetAsset(id string) ([]byte, error) {
return p.mediaBucket.Get(id)
}
func (p *PluginManager) Launch(configuration *app.Config) {
log.Info("start plugin manager daemon...")
// init redis client
if configuration.RedisUseSentinel {
// use Redis Sentinel
sentinels := strings.Split(configuration.RedisSentinels, ",")
if err := cache.InitRedisSentinelClient(
sentinels,
configuration.RedisSentinelServiceName,
configuration.RedisUser,
configuration.RedisPass,
configuration.RedisSentinelUsername,
configuration.RedisSentinelPassword,
configuration.RedisUseSsl,
configuration.RedisDB,
configuration.RedisSentinelSocketTimeout,
); err != nil {
log.Panic("init redis sentinel client failed: %s", err.Error())
}
} else {
if err := cache.InitRedisClient(
fmt.Sprintf("%s:%d", configuration.RedisHost, configuration.RedisPort),
configuration.RedisUser,
configuration.RedisPass,
configuration.RedisUseSsl,
configuration.RedisDB,
); err != nil {
log.Panic("init redis client failed: %s", err.Error())
}
}
invocation, err := calldify.NewDifyInvocationDaemon(
calldify.NewDifyInvocationDaemonPayload{
BaseUrl: configuration.DifyInnerApiURL,
CallingKey: configuration.DifyInnerApiKey,
WriteTimeout: configuration.DifyInvocationWriteTimeout,
ReadTimeout: configuration.DifyInvocationReadTimeout,
},
)
if err != nil {
log.Panic("init dify invocation daemon failed: %s", err.Error())
}
p.backwardsInvocation = invocation
// start control panel
p.controlPanel.StartWatchDog()
// launch serverless connector
if configuration.Platform == app.PLATFORM_SERVERLESS {
serverless.Init(configuration)
}
}
func (p *PluginManager) BackwardsInvocation() dify_invocation.BackwardsInvocation {
return p.backwardsInvocation
}
// check if the plugin is already running on this node
func (c *PluginManager) NeedRedirecting(
identity plugin_entities.PluginUniqueIdentifier,
) (bool, error) {
// debugging runtime were stored in control panel
if identity.RemoteLike() {
_, err := c.controlPanel.GetPluginRuntime(identity)
if err != nil {
return true, err
}
return false, nil
}
if c.config.Platform == app.PLATFORM_SERVERLESS {
// under serverless mode, it's no need to do redirecting
return false, nil
} else if c.config.Platform == app.PLATFORM_LOCAL {
// under local mode, check if the plugin is already running on this node
_, err := c.controlPanel.GetPluginRuntime(identity)
if err != nil {
// not found on this node, need to redirecting
return true, err
}
// found on current node
return false, nil
}
return true, nil
}
var (
pluginAssetCache *lru.Cache[string, []byte]
)
func pluginAssetCacheKey(
pluginUniqueIdentifier plugin_entities.PluginUniqueIdentifier,
path string,
) string {
return fmt.Sprintf("%s/%s", pluginUniqueIdentifier.String(), path)
}
func (p *PluginManager) ExtractPluginAsset(
pluginUniqueIdentifier plugin_entities.PluginUniqueIdentifier,
path string,
) ([]byte, error) {
key := pluginAssetCacheKey(pluginUniqueIdentifier, path)
cached, ok := pluginAssetCache.Get(key)
if ok {
return cached, nil
}
pkgBytes, err := manager.GetPackage(pluginUniqueIdentifier)
if err != nil {
return nil, err
}
zipDecoder, err := decoder.NewZipPluginDecoder(pkgBytes)
if err != nil {
return nil, err
}
assets, err := zipDecoder.Assets()
if err != nil {
return nil, err
}
pluginAssetCache.Add(key, assets[path])
return assets[path], nil
}