mirror of
https://github.com/langgenius/dify-plugin-daemon.git
synced 2026-07-22 01:35:24 -04:00
4314b8f1ee
Remote debugging plugins were not being synchronized across cluster nodes,
causing "no plugin available nodes found" errors when trying to invoke
plugins from different nodes.
1. **Remote debugging plugins not registered to cluster** - The
`ClusterTunnel` notifier was not being added to ControlPanel
2. **Plugin ID inconsistency** - Remote plugins used different plugin_id
formats during installation vs. querying
3. **Non-idempotent registration** - `RegisterPlugin` failed on reconnection
with "plugin has been registered" error
- **internal/types/models/curd/atomic.go**:
- Unify plugin_id calculation for remote plugins (author/name without version)
- Remove plugin_id from plugin query conditions
- Clear old cache when plugin_id is updated
- **internal/cluster/plugin.go**:
- Make `RegisterPlugin` idempotent by updating existing plugin instead
of returning error
- **internal/core/control_panel/daemon.go**:
- Add cluster field to ControlPanel
- Add SetCluster() method for lazy cluster initialization
- **internal/core/control_panel/server_debugger.go**:
- Register remote debugging plugins to cluster on connection
- Unregister from cluster on disconnection
- **internal/core/plugin_manager/manager.go**:
- Add SetCluster() method to set cluster after initialization
- **internal/server/server.go**:
- Call SetCluster() instead of AddClusterTunnel()
Only remote debugging plugins are synchronized across cluster nodes.
Local plugins run only on the node where they are installed and are
not registered to the cluster.
- Error handling improvements using `errors.Is()` instead of `==`
- Handle 404 for missing plugin assets gracefully
- Handle already-installed debugging plugins gracefully
- Remote debugging plugin can be invoked from any node in the cluster
- Plugin reconnection works without errors
- Cache invalidation works correctly when plugin_id changes
192 lines
5.3 KiB
Go
192 lines
5.3 KiB
Go
package server
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/langgenius/dify-plugin-daemon/internal/db"
|
|
"github.com/langgenius/dify-plugin-daemon/internal/server/constants"
|
|
"github.com/langgenius/dify-plugin-daemon/internal/types/exception"
|
|
"github.com/langgenius/dify-plugin-daemon/internal/types/models"
|
|
"github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities"
|
|
"github.com/langgenius/dify-plugin-daemon/pkg/utils/cache"
|
|
"github.com/langgenius/dify-plugin-daemon/pkg/utils/cache/helper"
|
|
"github.com/langgenius/dify-plugin-daemon/pkg/utils/log"
|
|
)
|
|
|
|
func CheckingKey(key string) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
// get header X-Api-Key
|
|
if c.GetHeader(constants.X_API_KEY) != key {
|
|
c.AbortWithStatusJSON(401, exception.UnauthorizedError().ToResponse())
|
|
return
|
|
}
|
|
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
func (app *App) FetchPluginInstallation() gin.HandlerFunc {
|
|
return func(ctx *gin.Context) {
|
|
pluginId := ctx.Request.Header.Get(constants.X_PLUGIN_ID)
|
|
if pluginId == "" {
|
|
ctx.AbortWithStatusJSON(400, exception.BadRequestError(errors.New("plugin_id is required")).ToResponse())
|
|
return
|
|
}
|
|
|
|
tenantId := ctx.Param("tenant_id")
|
|
if tenantId == "" {
|
|
ctx.AbortWithStatusJSON(400, exception.BadRequestError(errors.New("tenant_id is required")).ToResponse())
|
|
return
|
|
}
|
|
|
|
// fetch plugin installation with caching
|
|
cacheKey := helper.PluginInstallationCacheKey(pluginId, tenantId)
|
|
installation, err := cache.AutoGetWithGetter(
|
|
cacheKey,
|
|
func() (*models.PluginInstallation, error) {
|
|
inst, err := db.GetOne[models.PluginInstallation](
|
|
db.Equal("tenant_id", tenantId),
|
|
db.Equal("plugin_id", pluginId),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &inst, nil
|
|
},
|
|
)
|
|
|
|
if errors.Is(err, db.ErrDatabaseNotFound) {
|
|
ctx.AbortWithStatusJSON(404, exception.ErrPluginNotFound().ToResponse())
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
ctx.AbortWithStatusJSON(500, exception.InternalServerError(err).ToResponse())
|
|
return
|
|
}
|
|
|
|
identity, err := plugin_entities.NewPluginUniqueIdentifier(installation.PluginUniqueIdentifier)
|
|
if err != nil {
|
|
ctx.AbortWithStatusJSON(400, exception.UniqueIdentifierError(err).ToResponse())
|
|
return
|
|
}
|
|
|
|
ctx.Set(constants.CONTEXT_KEY_PLUGIN_INSTALLATION, *installation)
|
|
ctx.Set(constants.CONTEXT_KEY_PLUGIN_UNIQUE_IDENTIFIER, identity)
|
|
ctx.Next()
|
|
}
|
|
}
|
|
|
|
// RedirectPluginInvoke redirects the request to the correct cluster node
|
|
func (app *App) RedirectPluginInvoke() gin.HandlerFunc {
|
|
return func(ctx *gin.Context) {
|
|
// get plugin unique identifier
|
|
identityAny, ok := ctx.Get(constants.CONTEXT_KEY_PLUGIN_UNIQUE_IDENTIFIER)
|
|
if !ok {
|
|
ctx.AbortWithStatusJSON(
|
|
500,
|
|
exception.InternalServerError(errors.New("plugin unique identifier not found")).ToResponse(),
|
|
)
|
|
return
|
|
}
|
|
|
|
identity, ok := identityAny.(plugin_entities.PluginUniqueIdentifier)
|
|
if !ok {
|
|
ctx.AbortWithStatusJSON(
|
|
500,
|
|
exception.InternalServerError(errors.New("failed to parse plugin unique identifier")).ToResponse(),
|
|
)
|
|
return
|
|
}
|
|
|
|
// check if plugin in current node
|
|
if needRedirecting, originalError := app.pluginManager.NeedRedirecting(identity); needRedirecting {
|
|
app.redirectPluginInvokeByPluginIdentifier(ctx, identity, originalError)
|
|
ctx.Abort()
|
|
} else {
|
|
ctx.Next()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (app *App) redirectPluginInvokeByPluginIdentifier(
|
|
ctx *gin.Context,
|
|
plugin_unique_identifier plugin_entities.PluginUniqueIdentifier,
|
|
originalError error,
|
|
) {
|
|
// try find the correct node
|
|
nodes, err := app.cluster.FetchPluginAvailableNodesById(plugin_unique_identifier.String())
|
|
if err != nil {
|
|
log.Error("Failed to fetch plugin nodes by id", "error", err)
|
|
ctx.AbortWithStatusJSON(
|
|
500,
|
|
exception.InternalServerError(
|
|
errors.New("failed to fetch plugin available nodes, "+originalError.Error()+", "+err.Error()),
|
|
).ToResponse(),
|
|
)
|
|
return
|
|
} else if len(nodes) == 0 {
|
|
log.Error("no plugin available nodes found", "plugin", plugin_unique_identifier.String())
|
|
ctx.AbortWithStatusJSON(
|
|
404,
|
|
exception.InternalServerError(
|
|
errors.New("no available node, "+originalError.Error()),
|
|
).ToResponse(),
|
|
)
|
|
return
|
|
}
|
|
|
|
// redirect to the correct node
|
|
nodeId := nodes[0]
|
|
statusCode, header, body, err := app.cluster.RedirectRequest(nodeId, ctx.Request)
|
|
if err != nil {
|
|
log.Error("redirect request failed", "error", err)
|
|
ctx.AbortWithStatusJSON(
|
|
500,
|
|
exception.InternalServerError(errors.New("redirect request failed: "+err.Error())).ToResponse(),
|
|
)
|
|
return
|
|
}
|
|
|
|
// set status code
|
|
ctx.Writer.WriteHeader(statusCode)
|
|
|
|
// set header
|
|
for key, values := range header {
|
|
for _, value := range values {
|
|
ctx.Writer.Header().Set(key, value)
|
|
}
|
|
}
|
|
|
|
defer func(body io.ReadCloser) {
|
|
err := body.Close()
|
|
if err != nil {
|
|
log.Error("body close failed", "error", err)
|
|
}
|
|
}(body)
|
|
|
|
if _, err := io.Copy(ctx.Writer, body); err != nil {
|
|
log.Error("failed to write response body", "error", err)
|
|
}
|
|
}
|
|
|
|
func (app *App) InitClusterID() gin.HandlerFunc {
|
|
return func(ctx *gin.Context) {
|
|
ctx.Set(constants.CONTEXT_KEY_CLUSTER_ID, app.cluster.ID())
|
|
ctx.Next()
|
|
}
|
|
}
|
|
|
|
func (app *App) AdminAPIKey(key string) gin.HandlerFunc {
|
|
return func(ctx *gin.Context) {
|
|
if ctx.GetHeader(constants.X_ADMIN_API_KEY) != key {
|
|
ctx.AbortWithStatusJSON(401, gin.H{"message": "unauthorized"})
|
|
return
|
|
}
|
|
|
|
ctx.Next()
|
|
}
|
|
}
|