mirror of
https://github.com/langgenius/dify-plugin-daemon.git
synced 2026-07-23 02:05:27 -04:00
64738319b8
* fix(agent_declaration): remove unnecessary 'json_schema' validation from OutputSchema * fix(endpoint): add error handling for missing endpoint configuration in ListPluginEndpoints * fix(endpoint): update error handling for missing endpoint configuration in ListPluginEndpoints
422 lines
12 KiB
Go
422 lines
12 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/langgenius/dify-plugin-daemon/internal/core/dify_invocation"
|
|
"github.com/langgenius/dify-plugin-daemon/internal/core/plugin_daemon"
|
|
"github.com/langgenius/dify-plugin-daemon/internal/core/plugin_daemon/access_types"
|
|
"github.com/langgenius/dify-plugin-daemon/internal/core/plugin_manager"
|
|
"github.com/langgenius/dify-plugin-daemon/internal/core/session_manager"
|
|
"github.com/langgenius/dify-plugin-daemon/internal/db"
|
|
"github.com/langgenius/dify-plugin-daemon/internal/service/install_service"
|
|
"github.com/langgenius/dify-plugin-daemon/internal/types/exception"
|
|
"github.com/langgenius/dify-plugin-daemon/internal/types/models"
|
|
"github.com/langgenius/dify-plugin-daemon/internal/utils/cache"
|
|
"github.com/langgenius/dify-plugin-daemon/internal/utils/cache/helper"
|
|
"github.com/langgenius/dify-plugin-daemon/internal/utils/encryption"
|
|
"github.com/langgenius/dify-plugin-daemon/internal/utils/routine"
|
|
"github.com/langgenius/dify-plugin-daemon/pkg/entities"
|
|
"github.com/langgenius/dify-plugin-daemon/pkg/entities/endpoint_entities"
|
|
"github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities"
|
|
"github.com/langgenius/dify-plugin-daemon/pkg/entities/requests"
|
|
)
|
|
|
|
func copyRequest(req *http.Request, hookId string, path string) (*bytes.Buffer, error) {
|
|
newReq := req.Clone(context.Background())
|
|
// get query params
|
|
queryParams := req.URL.Query()
|
|
|
|
// replace path with endpoint path
|
|
newReq.URL.Path = path
|
|
// set query params
|
|
newReq.URL.RawQuery = queryParams.Encode()
|
|
|
|
// read request body until complete, max 10MB
|
|
body, err := io.ReadAll(io.LimitReader(req.Body, 10*1024*1024))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// replace with a new reader
|
|
newReq.Body = io.NopCloser(bytes.NewReader(body))
|
|
newReq.ContentLength = int64(len(body))
|
|
newReq.TransferEncoding = nil
|
|
|
|
// remove ip traces for security
|
|
newReq.Header.Del("X-Forwarded-For")
|
|
newReq.Header.Del("X-Real-IP")
|
|
newReq.Header.Del("X-Forwarded")
|
|
newReq.Header.Del("X-Original-Forwarded-For")
|
|
newReq.Header.Del("X-Original-Url")
|
|
newReq.Header.Del("X-Original-Host")
|
|
|
|
// check if X-Original-Host is set
|
|
if originalHost := req.Header.Get(endpoint_entities.HeaderXOriginalHost); originalHost != "" {
|
|
// replace host with original host
|
|
newReq.Host = originalHost
|
|
}
|
|
|
|
// setup hook id to request
|
|
newReq.Header.Set("Dify-Hook-Id", hookId)
|
|
// check if Dify-Hook-Url is set
|
|
if url := req.Header.Get("Dify-Hook-Url"); url == "" {
|
|
newReq.Header.Set(
|
|
"Dify-Hook-Url",
|
|
fmt.Sprintf("http://%s/e/%s%s", newReq.Host, hookId, path),
|
|
)
|
|
}
|
|
|
|
var buffer bytes.Buffer
|
|
err = newReq.Write(&buffer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &buffer, nil
|
|
}
|
|
|
|
func Endpoint(
|
|
ctx *gin.Context,
|
|
endpoint *models.Endpoint,
|
|
pluginInstallation *models.PluginInstallation,
|
|
maxExecutionTime time.Duration,
|
|
path string,
|
|
) {
|
|
if !endpoint.Enabled {
|
|
ctx.JSON(404, exception.NotFoundError(errors.New("endpoint not found")).ToResponse())
|
|
return
|
|
}
|
|
|
|
buffer, err := copyRequest(ctx.Request, endpoint.HookID, path)
|
|
if err != nil {
|
|
ctx.JSON(500, exception.InternalServerError(err).ToResponse())
|
|
return
|
|
}
|
|
|
|
identifier, err := plugin_entities.NewPluginUniqueIdentifier(pluginInstallation.PluginUniqueIdentifier)
|
|
if err != nil {
|
|
ctx.JSON(400, exception.UniqueIdentifierError(err).ToResponse())
|
|
return
|
|
}
|
|
|
|
// fetch plugin
|
|
manager := plugin_manager.Manager()
|
|
runtime, err := manager.Get(identifier)
|
|
if err != nil {
|
|
ctx.JSON(404, exception.ErrPluginNotFound().ToResponse())
|
|
return
|
|
}
|
|
|
|
// fetch endpoint declaration
|
|
endpointDeclaration := runtime.Configuration().Endpoint
|
|
if endpointDeclaration == nil {
|
|
ctx.JSON(404, exception.ErrPluginNotFound().ToResponse())
|
|
return
|
|
}
|
|
|
|
// decrypt settings
|
|
settings, err := manager.BackwardsInvocation().InvokeEncrypt(&dify_invocation.InvokeEncryptRequest{
|
|
BaseInvokeDifyRequest: dify_invocation.BaseInvokeDifyRequest{
|
|
TenantId: endpoint.TenantID,
|
|
UserId: "",
|
|
Type: dify_invocation.INVOKE_TYPE_ENCRYPT,
|
|
},
|
|
InvokeEncryptSchema: dify_invocation.InvokeEncryptSchema{
|
|
Opt: dify_invocation.ENCRYPT_OPT_DECRYPT,
|
|
Namespace: dify_invocation.ENCRYPT_NAMESPACE_ENDPOINT,
|
|
Identity: endpoint.ID,
|
|
Data: endpoint.Settings,
|
|
Config: endpointDeclaration.Settings,
|
|
},
|
|
})
|
|
|
|
if err != nil {
|
|
ctx.JSON(500, exception.InternalServerError(err).ToResponse())
|
|
return
|
|
}
|
|
|
|
session := session_manager.NewSession(
|
|
session_manager.NewSessionPayload{
|
|
TenantID: endpoint.TenantID,
|
|
UserID: "",
|
|
PluginUniqueIdentifier: identifier,
|
|
ClusterID: ctx.GetString("cluster_id"),
|
|
InvokeFrom: access_types.PLUGIN_ACCESS_TYPE_ENDPOINT,
|
|
Action: access_types.PLUGIN_ACCESS_ACTION_INVOKE_ENDPOINT,
|
|
Declaration: runtime.Configuration(),
|
|
BackwardsInvocation: manager.BackwardsInvocation(),
|
|
IgnoreCache: false,
|
|
EndpointID: &endpoint.ID,
|
|
},
|
|
)
|
|
defer session.Close(session_manager.CloseSessionPayload{
|
|
IgnoreCache: false,
|
|
})
|
|
|
|
session.BindRuntime(runtime)
|
|
|
|
statusCode, headers, response, err := plugin_daemon.InvokeEndpoint(
|
|
session, &requests.RequestInvokeEndpoint{
|
|
RawHttpRequest: hex.EncodeToString(buffer.Bytes()),
|
|
Settings: settings,
|
|
},
|
|
)
|
|
if err != nil {
|
|
ctx.JSON(500, exception.InternalServerError(err).ToResponse())
|
|
return
|
|
}
|
|
defer response.Close()
|
|
|
|
done := make(chan bool)
|
|
closed := new(int32)
|
|
|
|
ctx.Status(statusCode)
|
|
for k, v := range *headers {
|
|
if len(v) > 0 {
|
|
ctx.Writer.Header().Set(k, v[0])
|
|
}
|
|
}
|
|
|
|
close := func() {
|
|
if atomic.CompareAndSwapInt32(closed, 0, 1) {
|
|
close(done)
|
|
}
|
|
}
|
|
defer close()
|
|
|
|
routine.Submit(map[string]string{
|
|
"module": "service",
|
|
"function": "Endpoint",
|
|
}, func() {
|
|
defer close()
|
|
for response.Next() {
|
|
chunk, err := response.Read()
|
|
if err != nil {
|
|
ctx.Writer.Write([]byte(err.Error()))
|
|
ctx.Writer.Flush()
|
|
return
|
|
}
|
|
ctx.Writer.Write(chunk)
|
|
ctx.Writer.Flush()
|
|
}
|
|
})
|
|
|
|
select {
|
|
case <-ctx.Writer.CloseNotify():
|
|
case <-done:
|
|
case <-time.After(maxExecutionTime):
|
|
ctx.JSON(500, exception.InternalServerError(errors.New("killed by timeout")).ToResponse())
|
|
}
|
|
}
|
|
|
|
func EnableEndpoint(endpoint_id string, tenant_id string) *entities.Response {
|
|
|
|
if err := install_service.EnabledEndpoint(endpoint_id, tenant_id); err != nil {
|
|
return exception.InternalServerError(errors.New("failed to enable endpoint")).ToResponse()
|
|
}
|
|
|
|
return entities.NewSuccessResponse(true)
|
|
}
|
|
|
|
func DisableEndpoint(endpoint_id string, tenant_id string) *entities.Response {
|
|
endpoint, err := install_service.DisabledEndpoint(endpoint_id, tenant_id)
|
|
if err != nil {
|
|
return exception.InternalServerError(errors.New("failed to disable endpoint")).ToResponse()
|
|
}
|
|
|
|
// invalidate endpoint cache
|
|
endpointCacheKey := helper.EndpointCacheKey(endpoint.HookID)
|
|
_, _ = cache.AutoDelete[models.Endpoint](endpointCacheKey)
|
|
|
|
return entities.NewSuccessResponse(true)
|
|
}
|
|
|
|
func ListEndpoints(tenant_id string, page int, page_size int) *entities.Response {
|
|
endpoints, err := db.GetAll[models.Endpoint](
|
|
db.Equal("tenant_id", tenant_id),
|
|
db.OrderBy("created_at", true),
|
|
db.Page(page, page_size),
|
|
)
|
|
if err != nil {
|
|
return exception.InternalServerError(fmt.Errorf("failed to list endpoints: %v", err)).ToResponse()
|
|
}
|
|
|
|
manager := plugin_manager.Manager()
|
|
if manager == nil {
|
|
return exception.InternalServerError(errors.New("failed to get plugin manager")).ToResponse()
|
|
}
|
|
|
|
// decrypt settings
|
|
for i, endpoint := range endpoints {
|
|
pluginInstallation, err := db.GetOne[models.PluginInstallation](
|
|
db.Equal("plugin_id", endpoint.PluginID),
|
|
db.Equal("tenant_id", tenant_id),
|
|
)
|
|
if err != nil {
|
|
// use empty settings and declaration for uninstalled plugins
|
|
endpoint.Settings = map[string]any{}
|
|
endpoint.Declaration = &plugin_entities.EndpointProviderDeclaration{
|
|
Settings: []plugin_entities.ProviderConfig{},
|
|
Endpoints: []plugin_entities.EndpointDeclaration{},
|
|
EndpointFiles: []string{},
|
|
}
|
|
endpoints[i] = endpoint
|
|
continue
|
|
}
|
|
|
|
pluginUniqueIdentifier, err := plugin_entities.NewPluginUniqueIdentifier(
|
|
pluginInstallation.PluginUniqueIdentifier,
|
|
)
|
|
if err != nil {
|
|
return exception.UniqueIdentifierError(
|
|
fmt.Errorf("failed to parse plugin unique identifier: %v", err),
|
|
).ToResponse()
|
|
}
|
|
|
|
pluginDeclaration, err := manager.GetDeclaration(
|
|
pluginUniqueIdentifier,
|
|
tenant_id,
|
|
plugin_entities.PluginRuntimeType(pluginInstallation.RuntimeType),
|
|
)
|
|
if err != nil {
|
|
return exception.InternalServerError(
|
|
fmt.Errorf("failed to get plugin declaration: %v", err),
|
|
).ToResponse()
|
|
}
|
|
|
|
if pluginDeclaration.Endpoint == nil {
|
|
return exception.NotFoundError(errors.New("plugin does not have an endpoint")).ToResponse()
|
|
}
|
|
|
|
decryptedSettings, err := manager.BackwardsInvocation().InvokeEncrypt(&dify_invocation.InvokeEncryptRequest{
|
|
BaseInvokeDifyRequest: dify_invocation.BaseInvokeDifyRequest{
|
|
TenantId: tenant_id,
|
|
UserId: "",
|
|
Type: dify_invocation.INVOKE_TYPE_ENCRYPT,
|
|
},
|
|
InvokeEncryptSchema: dify_invocation.InvokeEncryptSchema{
|
|
Opt: dify_invocation.ENCRYPT_OPT_DECRYPT,
|
|
Namespace: dify_invocation.ENCRYPT_NAMESPACE_ENDPOINT,
|
|
Identity: endpoint.ID,
|
|
Data: endpoint.Settings,
|
|
Config: pluginDeclaration.Endpoint.Settings,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return exception.InternalServerError(
|
|
fmt.Errorf("failed to decrypt settings: %v", err),
|
|
).ToResponse()
|
|
}
|
|
|
|
// mask settings
|
|
decryptedSettings = encryption.MaskConfigCredentials(decryptedSettings, pluginDeclaration.Endpoint.Settings)
|
|
|
|
endpoint.Settings = decryptedSettings
|
|
endpoint.Declaration = pluginDeclaration.Endpoint
|
|
|
|
endpoints[i] = endpoint
|
|
}
|
|
|
|
return entities.NewSuccessResponse(endpoints)
|
|
}
|
|
|
|
func ListPluginEndpoints(tenant_id string, plugin_id string, page int, page_size int) *entities.Response {
|
|
endpoints, err := db.GetAll[models.Endpoint](
|
|
db.Equal("plugin_id", plugin_id),
|
|
db.Equal("tenant_id", tenant_id),
|
|
db.OrderBy("created_at", true),
|
|
db.Page(page, page_size),
|
|
)
|
|
if err != nil {
|
|
return exception.InternalServerError(
|
|
fmt.Errorf("failed to list endpoints: %v", err),
|
|
).ToResponse()
|
|
}
|
|
|
|
manager := plugin_manager.Manager()
|
|
if manager == nil {
|
|
return exception.InternalServerError(
|
|
errors.New("failed to get plugin manager"),
|
|
).ToResponse()
|
|
}
|
|
|
|
// decrypt settings
|
|
for i, endpoint := range endpoints {
|
|
// get installation
|
|
pluginInstallation, err := db.GetOne[models.PluginInstallation](
|
|
db.Equal("plugin_id", plugin_id),
|
|
db.Equal("tenant_id", tenant_id),
|
|
)
|
|
if err != nil {
|
|
return exception.NotFoundError(
|
|
fmt.Errorf("failed to find plugin installation: %v", err),
|
|
).ToResponse()
|
|
}
|
|
|
|
pluginUniqueIdentifier, err := plugin_entities.NewPluginUniqueIdentifier(
|
|
pluginInstallation.PluginUniqueIdentifier,
|
|
)
|
|
|
|
if err != nil {
|
|
return exception.UniqueIdentifierError(
|
|
fmt.Errorf("failed to parse plugin unique identifier: %v", err),
|
|
).ToResponse()
|
|
}
|
|
|
|
pluginDeclaration, err := manager.GetDeclaration(
|
|
pluginUniqueIdentifier,
|
|
tenant_id,
|
|
plugin_entities.PluginRuntimeType(pluginInstallation.RuntimeType),
|
|
)
|
|
if err != nil {
|
|
return exception.InternalServerError(
|
|
fmt.Errorf("failed to get plugin declaration: %v", err),
|
|
).ToResponse()
|
|
}
|
|
|
|
if pluginDeclaration.Endpoint == nil {
|
|
return exception.NotFoundError(errors.New("plugin does not have an endpoint")).ToResponse()
|
|
}
|
|
|
|
decryptedSettings, err := manager.BackwardsInvocation().InvokeEncrypt(&dify_invocation.InvokeEncryptRequest{
|
|
BaseInvokeDifyRequest: dify_invocation.BaseInvokeDifyRequest{
|
|
TenantId: tenant_id,
|
|
UserId: "",
|
|
Type: dify_invocation.INVOKE_TYPE_ENCRYPT,
|
|
},
|
|
InvokeEncryptSchema: dify_invocation.InvokeEncryptSchema{
|
|
Opt: dify_invocation.ENCRYPT_OPT_DECRYPT,
|
|
Namespace: dify_invocation.ENCRYPT_NAMESPACE_ENDPOINT,
|
|
Identity: endpoint.ID,
|
|
Data: endpoint.Settings,
|
|
Config: pluginDeclaration.Endpoint.Settings,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return exception.InternalServerError(
|
|
fmt.Errorf("failed to decrypt settings: %v", err),
|
|
).ToResponse()
|
|
}
|
|
|
|
// mask settings
|
|
decryptedSettings = encryption.MaskConfigCredentials(decryptedSettings, pluginDeclaration.Endpoint.Settings)
|
|
|
|
endpoint.Settings = decryptedSettings
|
|
endpoint.Declaration = pluginDeclaration.Endpoint
|
|
|
|
endpoints[i] = endpoint
|
|
}
|
|
|
|
return entities.NewSuccessResponse(endpoints)
|
|
}
|