Files
Maries 4589841b0c feat: introduce trigger (#482)
* feat(plugins): add FetchPluginReadme endpoint and update launch configurations

* feat: add PluginReadme database model

* feat: implement readme extracting and storage

* feat: implement readme endpoint

* feat: add plugin asset extraction endpoint with caching support

* Implement trigger functionality and clean up dynamic select code

- Added new trigger-related access types and actions in access.go.
- Introduced new HTTP routes for trigger operations in http_server.gen.go.
- Updated plugin declaration to include triggers in plugin_entities.
- Removed unused dynamic select service and controller files.
- Enhanced message handling in debugging_runtime to support trigger registration.

This update enhances the plugin system by integrating trigger capabilities while cleaning up legacy code.

* Refactor trigger-related types and enhance dynamic select functionality

- Updated TriggerProviderIdentity and TriggerProviderConfiguration to improve structure and validation.
- Renamed TriggerConfiguration to TriggerDeclaration for clarity.
- Added CredentialType to RequestDynamicParameterSelect for better request handling.
- Enhanced PluginDecoderHelper to read and unmarshal trigger files correctly.

These changes streamline the trigger system and improve the overall request handling in the plugin architecture.

* Add trigger functionality and enhance database integration

- Introduced TriggerInstallation model for managing trigger installations in the database.
- Updated autoMigrate function to include trigger installations in the migration process.
- Added new HTTP routes for listing and retrieving triggers in the HTTP server.
- Implemented ListTriggers and GetTrigger controller functions for handling trigger requests.
- Enhanced plugin management functions to create, update, and delete trigger installations during plugin lifecycle events.

These changes integrate trigger capabilities into the system, improving the overall plugin functionality and management.

* feat: add remapping for trigger icons in MediaBucket

- Enhanced the RemapAssets function to include remapping of trigger identity icons and dark icons.
- Added error handling for remapping failures to ensure robust asset management.

These changes improve the handling of trigger assets within the plugin system, ensuring icons are correctly remapped as needed.

* feat: add Multiple field to TriggerParameter for enhanced configuration

- Introduced a new Multiple field in the TriggerParameter struct to allow for multiple values in trigger configurations.
- This addition improves the flexibility of trigger parameters within the plugin system.

These changes enhance the capability of trigger parameters, enabling more complex configurations.

* feat: add Multiple field to ProviderConfig for enhanced configuration

- Introduced a new Multiple field in the ProviderConfig struct to allow for multiple values in provider configurations.
- This addition improves the flexibility of provider options within the plugin system.

These changes enhance the capability of provider configurations, enabling more complex setups.

* fix(plugin): update validation error messages in ManifestValidate method

- Enhanced error messages in the ManifestValidate function to include 'trigger' in the validation checks for plugin declarations.
- Updated logic to ensure that all relevant fields are considered when validating the presence of mutually exclusive parameters.

* feat(trigger): add CHECKBOX parameter type to plugin entities  and refactor the trigger provider strcuture

- Introduced a new CHECKBOX parameter type in constant.go for plugin entities.
- Updated tool_declaration.go and trigger_declaration.go to include TOOL_PARAMETER_TYPE_CHECKBOX and TRIGGER_PARAMETER_TYPE_CHECKBOX respectively.
- Enhanced validation logic to accommodate the new CHECKBOX type in parameter checks.

* fix(trigger): update SubscriptionSchema validation in TriggerProviderDeclaration

- Changed SubscriptionSchema validation from 'omitempty' to 'required' in TriggerProviderDeclaration to ensure it is always provided.
- Updated SubscriptionConstructor field to be a pointer to allow for optional inclusion in the trigger provider configuration.

* fix(trigger): rename ParametersSchema to Parameters in SubscriptionConstructor

- Updated the SubscriptionConstructor struct to rename the ParametersSchema field to Parameters for consistency.
- Adjusted related JSON and YAML marshaling logic to reflect the new field name, ensuring proper handling of trigger parameters.

* refactor(trigger): enhance YAML unmarshalling for SubscriptionConstructor and SubscriptionSchema

- Introduced a new helper function to convert YAML nodes to ProviderConfig lists, improving the handling of subscription_schema and credentials_schema.
- Updated the UnmarshalYAML method to utilize the new function, simplifying the logic for parsing different YAML formats.
- Ensured proper initialization of SubscriptionConstructor fields to prevent nil pointer dereferences.

* fix(trigger): update SubscriptionConstructor validation in TriggerProviderDeclaration

- Changed the validation for SubscriptionConstructor in TriggerProviderDeclaration from 'omitempty,dive' to 'omitempty' to simplify the validation logic.
- Ensured that the SubscriptionConstructor field remains optional while maintaining its intended functionality.

* refactor(trigger): rename Trigger to Event in plugin entities and related structures

- Updated the naming conventions in trigger_declaration.go to replace 'Trigger' with 'Event' for better clarity and consistency.
- Adjusted related types, validation functions, and unmarshalling logic to reflect the new 'Event' terminology.
- Ensured that all references to triggers in the codebase are updated to events, including in the SubscriptionConstructor and response structures.

* refactor(trigger): rename TriggerInvoke to TriggerInvokeEvent and update related structures

- Renamed TriggerInvoke function and associated request/response types to TriggerInvokeEvent for improved clarity.
- Updated routing and controller methods to reflect the new naming convention.
- Ensured all references to the trigger invoke functionality are consistent with the new event terminology.

* refactor(trigger): remove Subscription struct from trigger_declaration.go and update TriggerDispatchEventRequest

- Removed the Subscription struct from trigger_declaration.go to streamline the codebase.
- Added Credentials field to TriggerDispatchEventRequest for enhanced functionality and clarity.
- Ensured that the changes maintain consistency with the existing naming conventions and structures.

* fix(trigger): improve nil checks for SubscriptionConstructor in TriggerProviderDeclaration

- Added nil checks for SubscriptionConstructor before accessing its fields to prevent potential nil pointer dereferences.
- Ensured that Parameters and CredentialsSchema are initialized only if SubscriptionConstructor is not nil, enhancing code robustness.

* fix(plugin): add recovery mechanism in OnTraffic to handle panics

- Introduced a deferred function in OnTraffic to recover from panics, logging the error and stack trace for better debugging.
- This enhancement improves the stability of the DifyServer by preventing crashes due to unexpected runtime errors.

* feat(trigger): add Subscription field to TriggerInvokeEventRequest

- Introduced a new Subscription field in the TriggerInvokeEventRequest struct to accommodate subscription data.
- Ensured the field is marked as required, enhancing the request's functionality and validation requirements.

* refactor(event): simplify EventDescription structure in EventDeclaration

- Removed the EventDescription struct and replaced it with a direct I18nObject field in EventDeclaration.
- This change streamlines the event configuration by reducing complexity while maintaining required validation for the description.

* feat(trigger): add UserID field to TriggerDispatchEventResponse

- Introduced a new UserID field in the TriggerDispatchEventResponse struct to include user identification in the response.
- The field is marked as optional, enhancing the response's flexibility while maintaining existing functionality.

* feat: add payload to TriggerDispatchEventResponse

* fix

* feat(trigger): update TriggerDispatchEventResponse structure

* fix: avoid path collusion

* fix: missing )

* fix: query param

* fix: form param

* fix: remove redundant dynamic parameter access type

* fix: remove dynamic parameter access type from validation

* Update internal/server/controllers/plugins.go

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

---------

Co-authored-by: Stream <Stream_2@qq.com>
Co-authored-by: Yeuoly <admin@srmxy.cn>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-10-29 13:56:58 +08:00

445 lines
14 KiB
Go

package plugin_entities
import (
"errors"
"strings"
"github.com/go-playground/locales/en"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
"github.com/langgenius/dify-plugin-daemon/pkg/validators"
)
type ConfigType string
const (
CONFIG_TYPE_SECRET_INPUT ConfigType = SECRET_INPUT
CONFIG_TYPE_TEXT_INPUT ConfigType = TEXT_INPUT
CONFIG_TYPE_SELECT ConfigType = SELECT
CONFIG_TYPE_BOOLEAN ConfigType = BOOLEAN
CONFIG_TYPE_MODEL_SELECTOR ConfigType = MODEL_SELECTOR
CONFIG_TYPE_APP_SELECTOR ConfigType = APP_SELECTOR
// CONFIG_TYPE_TOOL_SELECTOR ConfigType = TOOL_SELECTOR
CONFIG_TYPE_TOOLS_SELECTOR ConfigType = TOOLS_SELECTOR
CONFIG_TYPE_ANY ConfigType = ANY
)
type ModelConfigScope string
const (
MODEL_CONFIG_SCOPE_ALL ModelConfigScope = "all"
MODEL_CONFIG_SCOPE_LLM ModelConfigScope = "llm"
MODEL_CONFIG_SCOPE_TEXT_EMBEDDING ModelConfigScope = "text-embedding"
MODEL_CONFIG_SCOPE_RERANK ModelConfigScope = "rerank"
MODEL_CONFIG_SCOPE_TTS ModelConfigScope = "tts"
MODEL_CONFIG_SCOPE_SPEECH2TEXT ModelConfigScope = "speech2text"
MODEL_CONFIG_SCOPE_MODERATION ModelConfigScope = "moderation"
MODEL_CONFIG_SCOPE_VISION ModelConfigScope = "vision"
MODEL_CONFIG_SCOPE_DOCUMENT ModelConfigScope = "document"
MODEL_CONFIG_SCOPE_TOOL_CALL ModelConfigScope = "tool-call"
)
type AppSelectorScope string
const (
APP_SELECTOR_SCOPE_ALL AppSelectorScope = "all"
APP_SELECTOR_SCOPE_CHAT AppSelectorScope = "chat"
APP_SELECTOR_SCOPE_WORKFLOW AppSelectorScope = "workflow"
APP_SELECTOR_SCOPE_COMPLETION AppSelectorScope = "completion"
)
type ToolSelectorScope string
const (
TOOL_SELECTOR_SCOPE_ALL ToolSelectorScope = "all"
TOOL_SELECTOR_SCOPE_PLUGIN ToolSelectorScope = "plugin"
TOOL_SELECTOR_SCOPE_API ToolSelectorScope = "api"
TOOL_SELECTOR_SCOPE_WORKFLOW ToolSelectorScope = "workflow"
)
type AnyScope string
const (
ANY_SCOPE_STRING AnyScope = "string"
ANY_SCOPE_NUMBER AnyScope = "number"
ANY_SCOPE_OBJECT AnyScope = "object"
ANY_SCOPE_ARRAY_NUMBER AnyScope = "array[number]"
ANY_SCOPE_ARRAY_STRING AnyScope = "array[string]"
ANY_SCOPE_ARRAY_OBJECT AnyScope = "array[object]"
ANY_SCOPE_ARRAY_FILES AnyScope = "array[file]"
)
func isCredentialType(fl validator.FieldLevel) bool {
value := fl.Field().String()
switch value {
case string(CONFIG_TYPE_SECRET_INPUT),
string(CONFIG_TYPE_TEXT_INPUT),
string(CONFIG_TYPE_SELECT),
string(CONFIG_TYPE_BOOLEAN),
string(CONFIG_TYPE_APP_SELECTOR),
string(CONFIG_TYPE_MODEL_SELECTOR),
string(CONFIG_TYPE_TOOLS_SELECTOR):
return true
}
return false
}
type ConfigOption struct {
Value string `json:"value" validate:"required,lt=128"`
Label I18nObject `json:"label" validate:"required"`
}
func isModelConfigScope(fl validator.FieldLevel) bool {
value := fl.Field().String()
// split by and symbol
scopes := strings.Split(value, "&")
for _, scope := range scopes {
// trim space
scope = strings.TrimSpace(scope)
switch scope {
case string(MODEL_CONFIG_SCOPE_LLM),
string(MODEL_CONFIG_SCOPE_TEXT_EMBEDDING),
string(MODEL_CONFIG_SCOPE_RERANK),
string(MODEL_CONFIG_SCOPE_TTS),
string(MODEL_CONFIG_SCOPE_SPEECH2TEXT),
string(MODEL_CONFIG_SCOPE_MODERATION),
string(MODEL_CONFIG_SCOPE_VISION),
string(MODEL_CONFIG_SCOPE_DOCUMENT),
string(MODEL_CONFIG_SCOPE_TOOL_CALL):
return true
}
}
return false
}
func isAppSelectorScope(fl validator.FieldLevel) bool {
value := fl.Field().String()
// split by and symbol
scopes := strings.Split(value, "&")
for _, scope := range scopes {
// trim space
scope = strings.TrimSpace(scope)
switch scope {
case string(APP_SELECTOR_SCOPE_ALL),
string(APP_SELECTOR_SCOPE_CHAT),
string(APP_SELECTOR_SCOPE_WORKFLOW),
string(APP_SELECTOR_SCOPE_COMPLETION):
return true
}
}
return false
}
func isToolSelectorScope(fl validator.FieldLevel) bool {
value := fl.Field().String()
// split by and symbol
scopes := strings.Split(value, "&")
for _, scope := range scopes {
// trim space
scope = strings.TrimSpace(scope)
switch scope {
case string(TOOL_SELECTOR_SCOPE_ALL),
string(TOOL_SELECTOR_SCOPE_PLUGIN),
string(TOOL_SELECTOR_SCOPE_API),
string(TOOL_SELECTOR_SCOPE_WORKFLOW):
return true
}
}
return false
}
func isVarSelectorScope(fl validator.FieldLevel) bool {
value := fl.Field().String()
// split by and symbol
scopes := strings.Split(value, "&")
for _, scope := range scopes {
// trim space
scope = strings.TrimSpace(scope)
switch scope {
case string(ANY_SCOPE_STRING),
string(ANY_SCOPE_NUMBER),
string(ANY_SCOPE_OBJECT),
string(ANY_SCOPE_ARRAY_NUMBER),
string(ANY_SCOPE_ARRAY_STRING),
string(ANY_SCOPE_ARRAY_OBJECT),
string(ANY_SCOPE_ARRAY_FILES):
return true
}
}
return false
}
func isScope(fl validator.FieldLevel) bool {
// get parent and check if it's a provider config
parent := fl.Parent().Interface()
if providerConfig, ok := parent.(ProviderConfig); ok {
// check config type
if providerConfig.Type == CONFIG_TYPE_APP_SELECTOR {
return isAppSelectorScope(fl)
} else if providerConfig.Type == CONFIG_TYPE_MODEL_SELECTOR {
return isModelConfigScope(fl)
} else if providerConfig.Type == CONFIG_TYPE_ANY {
return isVarSelectorScope(fl)
} else {
return false
}
//else if providerConfig.Type == CONFIG_TYPE_TOOL_SELECTOR {
//return isToolSelectorScope(fl)
//}
}
if toolParameter, ok := parent.(ToolParameter); ok {
if toolParameter.Type == TOOL_PARAMETER_TYPE_APP_SELECTOR {
return isAppSelectorScope(fl)
} else if toolParameter.Type == TOOL_PARAMETER_TYPE_MODEL_SELECTOR {
return isModelConfigScope(fl)
} else if toolParameter.Type == TOOL_PARAMETER_TYPE_ANY {
return isVarSelectorScope(fl)
} else {
return false
}
// else if toolParameter.Type == TOOL_PARAMETER_TYPE_TOOL_SELECTOR {
// return isToolSelectorScope(fl)
// }
}
if agentStrategyParameter, ok := parent.(AgentStrategyParameter); ok {
if agentStrategyParameter.Type == AGENT_STRATEGY_PARAMETER_TYPE_APP_SELECTOR {
return isAppSelectorScope(fl)
} else if agentStrategyParameter.Type == AGENT_STRATEGY_PARAMETER_TYPE_MODEL_SELECTOR {
return isModelConfigScope(fl)
} else if agentStrategyParameter.Type == AGENT_STRATEGY_PARAMETER_TYPE_ANY {
return isVarSelectorScope(fl)
} else {
return false
}
//else if agentStrategyParameter.Type == AGENT_STRATEGY_PARAMETER_TYPE_TOOLS_SELECTOR {
// return isToolSelectorScope(fl)
//}
}
return false
}
func init() {
en := en.New()
uni := ut.New(en, en)
translator, _ := uni.GetTranslator("en")
validators.GlobalEntitiesValidator.RegisterValidation("is_scope", isScope)
validators.GlobalEntitiesValidator.RegisterTranslation(
"is_scope",
translator,
func(ut ut.Translator) error {
return ut.Add("is_scope", "{0} is not a valid scope", true)
},
func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("is_scope", fe.Field())
return t
},
)
validators.GlobalEntitiesValidator.RegisterValidation("is_app_selector_scope", isAppSelectorScope)
validators.GlobalEntitiesValidator.RegisterTranslation(
"is_app_selector_scope",
translator,
func(ut ut.Translator) error {
return ut.Add("is_app_selector_scope", "{0} is not a valid app selector scope", true)
},
func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("is_app_selector_scope", fe.Field())
return t
},
)
validators.GlobalEntitiesValidator.RegisterValidation("is_model_config_scope", isModelConfigScope)
validators.GlobalEntitiesValidator.RegisterTranslation(
"is_model_config_scope",
translator,
func(ut ut.Translator) error {
return ut.Add("is_model_config_scope", "{0} is not a valid model config scope", true)
},
func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("is_model_config_scope", fe.Field())
return t
},
)
validators.GlobalEntitiesValidator.RegisterValidation("is_tool_selector_scope", isToolSelectorScope)
validators.GlobalEntitiesValidator.RegisterTranslation(
"is_tool_selector_scope",
translator,
func(ut ut.Translator) error {
return ut.Add("is_tool_selector_scope", "{0} is not a valid tool selector scope", true)
},
func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("is_tool_selector_scope", fe.Field())
return t
},
)
}
type ProviderConfig struct {
Name string `json:"name" validate:"omitempty,gt=0,lt=1024"`
Type ConfigType `json:"type" validate:"required,credential_type"`
Scope *string `json:"scope" validate:"omitempty,is_scope"`
Required bool `json:"required"`
Default any `json:"default" validate:"omitempty,is_basic_type"`
Options []ConfigOption `json:"options" validate:"omitempty,lt=128,dive"`
Multiple bool `json:"multiple" validate:"omitempty"`
Label I18nObject `json:"label" validate:"required"`
Help *I18nObject `json:"help" validate:"omitempty"`
URL *string `json:"url" validate:"omitempty"`
Placeholder *I18nObject `json:"placeholder" validate:"omitempty"`
}
func init() {
en := en.New()
uni := ut.New(en, en)
translator, _ := uni.GetTranslator("en")
validators.GlobalEntitiesValidator.RegisterValidation("credential_type", isCredentialType)
validators.GlobalEntitiesValidator.RegisterTranslation(
"credential_type",
translator,
func(ut ut.Translator) error {
return ut.Add("credential_type", "{0} is not a valid credential type", true)
},
func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("credential_type", fe.Field())
return t
},
)
}
// ValidateProviderConfigs validates the provider configs
func ValidateProviderConfigs(settings map[string]any, configs []ProviderConfig) error {
if len(settings) > 64 {
return errors.New("too many setting fields")
}
configsMap := make(map[string]ProviderConfig)
for _, config := range configs {
configsMap[config.Name] = config
}
for config_name, config := range configsMap {
v, ok := settings[config_name]
if (!ok || v == nil) && config.Required {
return errors.New("missing required setting: " + config_name)
}
if !ok || v == nil {
continue
}
// check type
switch config.Type {
case CONFIG_TYPE_TEXT_INPUT:
if _, ok := v.(string); !ok {
return errors.New("setting " + config_name + " is not a string")
}
case CONFIG_TYPE_SECRET_INPUT:
if _, ok := v.(string); !ok {
return errors.New("setting " + config_name + " is not a string")
}
case CONFIG_TYPE_SELECT:
if _, ok := v.(string); !ok {
return errors.New("setting " + config_name + " is not a string")
}
// check if value is in options
found := false
for _, option := range config.Options {
if v == option.Value {
found = true
break
}
}
if !found {
return errors.New("setting " + config_name + " is not a valid option")
}
case CONFIG_TYPE_BOOLEAN:
if _, ok := v.(bool); !ok {
return errors.New("setting " + config_name + " is not a boolean")
}
case CONFIG_TYPE_APP_SELECTOR:
m, ok := v.(map[string]any)
if !ok {
return errors.New("setting " + config_name + " is not a map")
}
// check keys
if _, ok := m["app_id"]; !ok {
return errors.New("setting " + config_name + " is missing app_id")
}
case CONFIG_TYPE_MODEL_SELECTOR:
m, ok := v.(map[string]any)
if !ok {
return errors.New("setting " + config_name + " is not a map")
}
// check keys
if _, ok := m["provider"]; !ok {
return errors.New("setting " + config_name + " is missing provider")
}
if _, ok := m["model"]; !ok {
return errors.New("setting " + config_name + " is missing model")
}
if _, ok := m["model_type"]; !ok {
return errors.New("setting " + config_name + " is missing model_type")
}
// check scope
if config.Scope != nil {
switch *config.Scope {
case string(MODEL_CONFIG_SCOPE_ALL):
// do nothing
case string(MODEL_CONFIG_SCOPE_LLM):
// do nothing
case string(MODEL_CONFIG_SCOPE_TEXT_EMBEDDING):
// do nothing
case string(MODEL_CONFIG_SCOPE_RERANK):
// score_threshold, top_n
if _, ok := m["score_threshold"]; !ok {
return errors.New("setting " + config_name + " is missing score_threshold")
}
if _, ok := m["top_n"]; !ok {
return errors.New("setting " + config_name + " is missing top_n")
}
case string(MODEL_CONFIG_SCOPE_TTS):
// voice
if _, ok := m["voice"]; !ok {
return errors.New("setting " + config_name + " is missing voice")
}
case string(MODEL_CONFIG_SCOPE_SPEECH2TEXT):
// do nothing
case string(MODEL_CONFIG_SCOPE_MODERATION):
// do nothing
case string(MODEL_CONFIG_SCOPE_VISION):
// the same as llm
if _, ok := m["completion_params"]; !ok {
return errors.New("setting " + config_name + " is missing completion_params")
}
default:
return errors.New("setting " + config_name + " is not a valid model config scope")
}
}
// case CONFIG_TYPE_TOOL_SELECTOR:
// m, ok := v.(map[string]any)
// if !ok {
// return errors.New("setting " + config_name + " is not a map")
// }
// // check keys
// if _, ok := m["provider"]; !ok {
// return errors.New("setting " + config_name + " is missing provider")
// }
// if _, ok := m["tool"]; !ok {
// return errors.New("setting " + config_name + " is missing tool")
// }
// if _, ok := m["tool_type"]; !ok {
// return errors.New("setting " + config_name + " is missing tool_type")
// }
}
}
return nil
}