diff --git a/.env.example b/.env.example index 071916ed91..75b44f5d28 100644 --- a/.env.example +++ b/.env.example @@ -11,7 +11,8 @@ PLUGIN_REMOTE_INSTALLING_HOST=127.0.0.1 PLUGIN_REMOTE_INSTALLING_PORT=5003 # s3 credentials -S3_USE_AWS_MANAGED_IAM=true +S3_USE_AWS=true +S3_USE_AWS_MANAGED_IAM=false S3_ENDPOINT= S3_USE_PATH_STYLE=true AWS_ACCESS_KEY= @@ -35,7 +36,23 @@ ALIYUN_OSS_PATH= AZURE_BLOB_STORAGE_CONTAINER_NAME= AZURE_BLOB_STORAGE_CONNECTION_STRING= +# volcengine tos +VOLCENGINE_TOS_ENDPOINT= +VOLCENGINE_TOS_ACCESS_KEY= +VOLCENGINE_TOS_SECRET_KEY= +VOLCENGINE_TOS_REGION= + +# gcs storage credentials base64 string +GCS_CREDENTIALS= + +# huawei obs credentials +HUAWEI_OBS_ACCESS_KEY= +HUAWEI_OBS_SECRET_KEY= +HUAWEI_OBS_SERVER= + + # services storage +# https://github.com/langgenius/dify-cloud-kit/blob/main/oss/factory/factory.go PLUGIN_STORAGE_TYPE=local PLUGIN_STORAGE_OSS_BUCKET= PLUGIN_STORAGE_LOCAL_ROOT=./storage @@ -62,6 +79,18 @@ REDIS_PORT=6379 REDIS_PASSWORD=difyai123456 REDIS_DB=0 +# Whether to use Redis Sentinel mode. +# If set to true, the application will automatically discover and connect to the master node through Sentinel. +REDIS_USE_SENTINEL=false + +# List of Redis Sentinel nodes. If Sentinel mode is enabled, provide at least one Sentinel IP and port. +# Format: `:,:,:` +REDIS_SENTINELS= +REDIS_SENTINEL_SERVICE_NAME= +REDIS_SENTINEL_USERNAME= +REDIS_SENTINEL_PASSWORD= +REDIS_SENTINEL_SOCKET_TIMEOUT=0.1 + DB_USERNAME=postgres DB_PASSWORD=difyai123456 DB_HOST=localhost @@ -75,6 +104,9 @@ DB_SSL_MODE=disable DB_MAX_IDLE_CONNS=10 DB_MAX_OPEN_CONNS=30 DB_CONN_MAX_LIFETIME=3600 +# DB_EXTRAS in GORM format +DB_EXTRAS= +DB_CHARSET= DIFY_INVOCATION_CONNECTION_IDLE_TIMEOUT=120 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..0743de2532 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- +**Self Checks** + +To make sure we get to you in time, please check the following :) +- [ ] I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify-plugin-daemon/issues), including closed ones. +- [ ] I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)). +- [ ] [FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:) +- [ ] "Please do not modify this template :) and fill in all the required fields." + +**Versions** +1. dify-plugin-daemon Version +2. dify-api Version + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..3ba13e0cec --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..bcaf2cac0b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,29 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Self Checks** + +To make sure we get to you in time, please check the following :) +- [ ] I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify-plugin-daemon/issues), including closed ones. +- [ ] I confirm that I am using English to submit this report (我已阅读并同意 [Language Policy](https://github.com/langgenius/dify/issues/1542)). +- [ ] [FOR CHINESE USERS] 请务必使用英文提交 Issue,否则会被关闭。谢谢!:) +- [ ] "Please do not modify this template :) and fill in all the required fields." + + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..59824f1950 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,25 @@ +## Description + +Please provide a brief description of the changes made in this pull request. +Please also include the issue number if this is related to an issue using the format `Fixes #123` or `Closes #123`. + +## Type of Change + +- [ ] Bug fix +- [ ] New feature +- [ ] Refactor +- [ ] Performance improvement +- [ ] Other + +## Essential Checklist + +### Testing +- [ ] I have tested the changes locally and confirmed they work as expected +- [ ] I have added unit tests where necessary and they pass successfully + +### Bug Fix (if applicable) +- [ ] I have used GitHub syntax to close the related issue (e.g., `Fixes #123` or `Closes #123`) + +## Additional Information + +Please provide any additional context that would help reviewers understand the changes. \ No newline at end of file diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index 5a13881f20..d1b76b9de9 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -7,6 +7,7 @@ on: - "deploy/dev" - "cache-test" - "dify-for-mysql" + - "build/**" pull_request: branches: - "main" @@ -32,7 +33,7 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - + - name: Set matrix id: set-matrix run: | diff --git a/README.md b/README.md index df61bf1f54..dafb6212e3 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ All requests from Dify api based on HTTP protocol, but depends on the runtime ty - For local runtime, daemon will start plugin as the subprocess and communicate with the plugin via STDIN/STDOUT. - For debug runtime, daemon wait for a plugin to connect and communicate in full-duplex way, it's TCP based. -- For serverless runtime, plugin will be packaged to a third-party service like AWS Lambda and then be invoked by the daemon via HTTP protocol. +- For serverless runtime, plugin will be packaged to a third-party service like AWS Lambda and then be invoked by the daemon via HTTP protocol. You may refer to [SRI Docs](./docs/runtime/sri.md) for more detailed information. For more detailed introduction about Dify plugin, please refer to our docs [https://docs.dify.ai/plugins/introduction](https://docs.dify.ai/plugins/introduction). @@ -48,10 +48,13 @@ Firstly copy the `.env.example` file to `.env` and set the correct environment v cp .env.example .env ``` +If you were using a non-AWS S3 storage before version 0.1.2, you need to manually set the S3_USE_AWS environment variable to false in the .env file. + Attention that the `PYTHON_INTERPRETER_PATH` is the path to the python interpreter, please specify the correct path according to your python installation and make sure the python version is 3.11 or higher, as dify-plugin-sdk requires. We recommend you to use `vscode` to debug the daemon, and a `launch.json` file is provided in the `.vscode` directory. + ### Python environment #### UV Daemon uses `uv` to manage the dependencies of plugins, before you start the daemon, you need to install [uv](https://github.com/astral-sh/uv) by yourself. @@ -71,7 +74,7 @@ uses docker volume to share the directory with the host machine, it's better for ### Kubernetes -For now, Daemon community edition dose not support smoothly scale out with the number of replicas, If you are interested in this feature, please contact us. we have a more production-ready version for enterprise users. +For now, Daemon community edition does not support smoothly scale out with the number of replicas, If you are interested in this feature, please contact us. we have a more production-ready version for enterprise users. ## Benchmark diff --git a/cmd/codegen/main.go b/cmd/codegen/main.go new file mode 100644 index 0000000000..11122a9a40 --- /dev/null +++ b/cmd/codegen/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/langgenius/dify-plugin-daemon/internal/server/controllers/generator" +) + +func main() { + // Parse command line flags + flag.Parse() + + // Generate all files + if err := generator.GenerateAll(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} diff --git a/cmd/commandline/plugin.go b/cmd/commandline/plugin.go index 8da27dc766..845f6598cf 100644 --- a/cmd/commandline/plugin.go +++ b/cmd/commandline/plugin.go @@ -37,28 +37,6 @@ var ( Long: `Initialize a new plugin with the given parameters. If no parameters are provided, an interactive mode will be started.`, Run: func(c *cobra.Command, args []string) { - author, _ := c.Flags().GetString("author") - name, _ := c.Flags().GetString("name") - repo, _ := c.Flags().GetString("repo") - description, _ := c.Flags().GetString("description") - allowRegisterEndpoint, _ := c.Flags().GetBool("allow-register-endpoint") - allowInvokeTool, _ := c.Flags().GetBool("allow-invoke-tool") - allowInvokeModel, _ := c.Flags().GetBool("allow-invoke-model") - allowInvokeLLM, _ := c.Flags().GetBool("allow-invoke-llm") - allowInvokeTextEmbedding, _ := c.Flags().GetBool("allow-invoke-text-embedding") - allowInvokeRerank, _ := c.Flags().GetBool("allow-invoke-rerank") - allowInvokeTTS, _ := c.Flags().GetBool("allow-invoke-tts") - allowInvokeSpeech2Text, _ := c.Flags().GetBool("allow-invoke-speech2text") - allowInvokeModeration, _ := c.Flags().GetBool("allow-invoke-moderation") - allowInvokeNode, _ := c.Flags().GetBool("allow-invoke-node") - allowInvokeApp, _ := c.Flags().GetBool("allow-invoke-app") - allowUseStorage, _ := c.Flags().GetBool("allow-use-storage") - storageSize, _ := c.Flags().GetUint64("storage-size") - category, _ := c.Flags().GetString("category") - language, _ := c.Flags().GetString("language") - minDifyVersion, _ := c.Flags().GetString("min-dify-version") - quick, _ := c.Flags().GetBool("quick") - plugin.InitPluginWithFlags( author, name, @@ -177,6 +155,23 @@ If no parameters are provided, an interactive mode will be started.`, }, } + pluginReadmeCommand = &cobra.Command{ + Use: "readme", + Short: "Readme", + Long: "Readme", + } + + pluginReadmeListCommand = &cobra.Command{ + Use: "list [plugin_path]", + Short: "List available README languages", + Long: "List available README languages in the specified plugin", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + pluginPath := args[0] + plugin.ListReadme(pluginPath) + }, + } + // NOTE: tester is deprecated, maybe, in several months, we will support this again // pluginTestCommand = &cobra.Command{ // Use: "test [-i inputs] [-t timeout] package_path invoke_type invoke_action", @@ -229,10 +224,12 @@ func init() { pluginCommand.AddCommand(pluginChecksumCommand) pluginCommand.AddCommand(pluginEditPermissionCommand) pluginCommand.AddCommand(pluginModuleCommand) + pluginCommand.AddCommand(pluginReadmeCommand) pluginModuleCommand.AddCommand(pluginModuleListCommand) pluginModuleCommand.AddCommand(pluginModuleAppendCommand) pluginModuleAppendCommand.AddCommand(pluginModuleAppendToolsCommand) pluginModuleAppendCommand.AddCommand(pluginModuleAppendEndpointsCommand) + pluginReadmeCommand.AddCommand(pluginReadmeListCommand) pluginInitCommand.Flags().StringVar(&author, "author", "", "Author name (1-64 characters, lowercase letters, numbers, dashes and underscores only)") pluginInitCommand.Flags().StringVar(&name, "name", "", "Plugin name (1-128 characters, lowercase letters, numbers, dashes and underscores only)") diff --git a/cmd/commandline/plugin/init.go b/cmd/commandline/plugin/init.go index fe343b1a26..e7fbf1684a 100644 --- a/cmd/commandline/plugin/init.go +++ b/cmd/commandline/plugin/init.go @@ -16,8 +16,58 @@ import ( "github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities" ) -//go:embed templates/python/icon.svg -var icon []byte +var ( + //go:embed templates/icons/agent_light.svg + agentLight []byte + //go:embed templates/icons/agent_dark.svg + agentDark []byte + //go:embed templates/icons/datasource_light.svg + datasourceLight []byte + //go:embed templates/icons/datasource_dark.svg + datasourceDark []byte + //go:embed templates/icons/extension_light.svg + extensionLight []byte + //go:embed templates/icons/extension_dark.svg + extensionDark []byte + //go:embed templates/icons/model_light.svg + modelLight []byte + //go:embed templates/icons/model_dark.svg + modelDark []byte + //go:embed templates/icons/tool_light.svg + toolLight []byte + //go:embed templates/icons/tool_dark.svg + toolDark []byte + //go:embed templates/icons/trigger_light.svg + triggerLight []byte + //go:embed templates/icons/trigger_dark.svg + triggerDark []byte + + //go:embed templates/readme/zh_Hans.md + zhHansReadme []byte + //go:embed templates/readme/ja_JP.md + jaJpReadme []byte + //go:embed templates/readme/pt_BR.md + ptBrReadme []byte +) + +var icon = map[string]map[string][]byte{ + "light": { + "agent-strategy": agentLight, + "datasource": datasourceLight, + "extension": extensionLight, + "model": modelLight, + "tool": toolLight, + "trigger": triggerLight, + }, + "dark": { + "agent-strategy": agentDark, + "datasource": datasourceDark, + "extension": extensionDark, + "model": modelDark, + "tool": toolDark, + "trigger": triggerDark, + }, +} func InitPlugin() { m := initialize() @@ -152,14 +202,22 @@ func InitPluginWithFlags( // Update permissions perm := m.subMenus[SUB_MENU_KEY_PERMISSION].(permission) - perm.UpdatePermission(plugin_entities.PluginPermissionRequirement{ - Endpoint: &plugin_entities.PluginPermissionEndpointRequirement{ + permissionRequirement := &plugin_entities.PluginPermissionRequirement{} + + if allowRegisterEndpoint { + permissionRequirement.Endpoint = &plugin_entities.PluginPermissionEndpointRequirement{ Enabled: allowRegisterEndpoint, - }, - Tool: &plugin_entities.PluginPermissionToolRequirement{ + } + } + + if allowInvokeTool { + permissionRequirement.Tool = &plugin_entities.PluginPermissionToolRequirement{ Enabled: allowInvokeTool, - }, - Model: &plugin_entities.PluginPermissionModelRequirement{ + } + } + + if allowInvokeModel { + permissionRequirement.Model = &plugin_entities.PluginPermissionModelRequirement{ Enabled: allowInvokeModel, LLM: allowInvokeLLM, TextEmbedding: allowInvokeTextEmbedding, @@ -167,18 +225,29 @@ func InitPluginWithFlags( TTS: allowInvokeTTS, Speech2text: allowInvokeSpeech2Text, Moderation: allowInvokeModeration, - }, - Node: &plugin_entities.PluginPermissionNodeRequirement{ + } + } + + if allowInvokeNode { + permissionRequirement.Node = &plugin_entities.PluginPermissionNodeRequirement{ Enabled: allowInvokeNode, - }, - App: &plugin_entities.PluginPermissionAppRequirement{ + } + } + + if allowInvokeApp { + permissionRequirement.App = &plugin_entities.PluginPermissionAppRequirement{ Enabled: allowInvokeApp, - }, - Storage: &plugin_entities.PluginPermissionStorageRequirement{ + } + } + + if allowUseStorage { + permissionRequirement.Storage = &plugin_entities.PluginPermissionStorageRequirement{ Enabled: allowUseStorage, Size: storageSize, - }, - }) + } + } + + perm.UpdatePermission(*permissionRequirement) m.subMenus[SUB_MENU_KEY_PERMISSION] = perm // If quick mode is enabled, skip interactive mode @@ -333,6 +402,7 @@ func (m model) createPlugin() { Version: manifest_entities.Version("0.0.1"), Type: manifest_entities.PluginType, Icon: "icon.svg", + IconDark: "icon-dark.svg", Author: m.subMenus[SUB_MENU_KEY_PROFILE].(profile).Author(), Name: m.subMenus[SUB_MENU_KEY_PROFILE].(profile).Name(), Description: plugin_entities.NewI18nObject(m.subMenus[SUB_MENU_KEY_PROFILE].(profile).Description()), @@ -420,12 +490,30 @@ func (m model) createPlugin() { return } + // get icon and icon-dark + iconLight := icon["light"][string(manifest.Category())] + if iconLight == nil { + log.Error("icon not found for category: %s", manifest.Category()) + return + } + iconDark := icon["dark"][string(manifest.Category())] + if iconDark == nil { + log.Error("icon-dark not found for category: %s", manifest.Category()) + return + } + // create icon.svg - if err := writeFile(filepath.Join(pluginDir, "_assets", "icon.svg"), string(icon)); err != nil { + if err := writeFile(filepath.Join(pluginDir, "_assets", "icon.svg"), string(iconLight)); err != nil { log.Error("failed to write icon file: %s", err) return } + // create icon-dark.svg + if err := writeFile(filepath.Join(pluginDir, "_assets", "icon-dark.svg"), string(iconDark)); err != nil { + log.Error("failed to write icon-dark file: %s", err) + return + } + // create README.md readme, err := renderTemplate(README, manifest, []string{}) if err != nil { @@ -437,6 +525,42 @@ func (m model) createPlugin() { return } + // create multilingual README files if enabled + profileMenu := m.subMenus[SUB_MENU_KEY_PROFILE].(profile) + if profileMenu.EnableI18nReadme() { + selectedLanguages := profileMenu.SelectedLanguages() + + // Define language template mapping + languageTemplates := map[string][]byte{ + "zh_Hans": zhHansReadme, + "ja_JP": jaJpReadme, + "pt_BR": ptBrReadme, + } + + for _, lang := range selectedLanguages { + if lang == "en" { + // English README is already created as README.md + continue + } + + if template, exists := languageTemplates[lang]; exists { + // Render the template for this language + langReadme, err := renderTemplate(template, manifest, []string{}) + if err != nil { + log.Error("failed to render %s README template: %s", lang, err) + return + } + + // Write the language-specific README file + readmeFilename := fmt.Sprintf("README_%s.md", lang) + if err := writeFile(filepath.Join(pluginDir, "readme", readmeFilename), langReadme); err != nil { + log.Error("failed to write %s README file: %s", lang, err) + return + } + } + } + } + // create .env.example if err := writeFile(filepath.Join(pluginDir, ".env.example"), string(ENV_EXAMPLE)); err != nil { log.Error("failed to write .env.example file: %s", err) diff --git a/cmd/commandline/plugin/list_readme.go b/cmd/commandline/plugin/list_readme.go new file mode 100644 index 0000000000..2ba37bc8b9 --- /dev/null +++ b/cmd/commandline/plugin/list_readme.go @@ -0,0 +1,86 @@ +package plugin + +import ( + "fmt" + "os" + "text/tabwriter" + + "github.com/langgenius/dify-plugin-daemon/internal/utils/log" + "github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/decoder" +) + +// Language represents supported README languages +type Language struct { + Code string + Name string + Available bool +} + +// GetLanguageName returns the full language name for a given language code +func GetLanguageName(code string) string { + languageNames := map[string]string{ + "en_US": "English", + "zh_Hans": "简体中文 (Simplified Chinese)", + "ja_JP": "日本語 (Japanese)", + "pt_BR": "Português (Portuguese - Brazil)", + } + + if name, exists := languageNames[code]; exists { + return name + } + return "unknown" +} + +// ListReadme displays README language information in table format for a specific plugin +func ListReadme(pluginPath string) { + var pluginDecoder decoder.PluginDecoder + var err error + + stat, err := os.Stat(pluginPath) + if err != nil { + log.Error("failed to get plugin file stat: %s", err) + return + } + + if stat.IsDir() { + pluginDecoder, err = decoder.NewFSPluginDecoder(pluginPath) + } else { + fileContent, err := os.ReadFile(pluginPath) + if err != nil { + log.Error("failed to read plugin file: %s", err) + return + } + pluginDecoder, err = decoder.NewZipPluginDecoder(fileContent) + if err != nil { + log.Error("failed to create zip plugin decoder: %s", err) + return + } + } + if err != nil { + log.Error("your plugin is not a valid plugin: %s", err) + return + } + + // Get available i18n README files + availableReadmes, err := pluginDecoder.AvailableI18nReadme() + if err != nil { + log.Error("failed to get available README files: %s", err) + return + } + + // Create a new tabwriter + w := tabwriter.NewWriter(os.Stdout, 0, 8, 3, ' ', 0) + + // Print table header + fmt.Fprintln(w, "language-code\tlanguage\tavailable") + fmt.Fprintln(w, "-------------\t--------\t---------") + + // Print each available README + for code, _ := range availableReadmes { + languageName := GetLanguageName(code) + fmt.Fprintf(w, "%s\t%s\t✅\n", code, languageName) + } + + // Flush the writer to ensure all output is printed + w.Flush() +} diff --git a/cmd/commandline/plugin/package.go b/cmd/commandline/plugin/package.go index c36a7b7517..f5a4fe1ae2 100644 --- a/cmd/commandline/plugin/package.go +++ b/cmd/commandline/plugin/package.go @@ -9,7 +9,7 @@ import ( ) var ( - MaxPluginPackageSize = int64(52428800) // 50MB + MaxPluginPackageSize = int64(50 * 1024 * 1024) // 50 MB ) func PackagePlugin(inputPath string, outputPath string) { @@ -24,7 +24,7 @@ func PackagePlugin(inputPath string, outputPath string) { zipFile, err := packager.Pack(MaxPluginPackageSize) if err != nil { - log.Error("failed to package plugin %v", err) + log.Error("failed to package plugin: %v", err) os.Exit(1) return } diff --git a/cmd/commandline/plugin/profile.go b/cmd/commandline/plugin/profile.go index 848422ea1d..e1e32d2b50 100644 --- a/cmd/commandline/plugin/profile.go +++ b/cmd/commandline/plugin/profile.go @@ -2,17 +2,28 @@ package plugin import ( "fmt" + "strings" ti "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities" ) +type languageChoice struct { + code string + name string + selected bool + required bool // English is required by default +} + type profile struct { cursor int inputs []ti.Model - warning string + enableI18nReadme bool + languageChoices []languageChoice + languageSection bool // true when in language selection section + warning string } func newProfile() profile { @@ -38,7 +49,15 @@ func newProfile() profile { repo.Prompt = "Repository URL (Optional) (press Enter to next step): " return profile{ - inputs: []ti.Model{name, author, description, repo}, + inputs: []ti.Model{name, author, description, repo}, + enableI18nReadme: false, + languageChoices: []languageChoice{ + {code: "en", name: "English", selected: true, required: true}, + {code: "zh_Hans", name: "简体中文 (Simplified Chinese)", selected: false, required: false}, + {code: "ja_JP", name: "日本語 (Japanese)", selected: false, required: false}, + {code: "pt_BR", name: "Português (Portuguese - Brazil)", selected: false, required: false}, + }, + languageSection: false, } } @@ -58,12 +77,80 @@ func (p profile) Repo() string { return p.inputs[3].Value() } +func (p profile) EnableI18nReadme() bool { + return p.enableI18nReadme +} + +func (p profile) SelectedLanguages() []string { + if !p.enableI18nReadme { + return []string{"en"} // Only English if i18n is disabled + } + + var selected []string + for _, choice := range p.languageChoices { + if choice.selected { + selected = append(selected, choice.code) + } + } + return selected +} + func (p profile) View() string { - s := fmt.Sprintf("Edit profile of the plugin\n%s\n%s\n%s\n%s\n", p.inputs[0].View(), p.inputs[1].View(), p.inputs[2].View(), p.inputs[3].View()) + var s strings.Builder + + s.WriteString("Edit profile of the plugin\n") + s.WriteString(fmt.Sprintf("%s\n%s\n%s\n%s\n", p.inputs[0].View(), p.inputs[1].View(), p.inputs[2].View(), p.inputs[3].View())) + + // Cursor helper function + cursor := func(isSelected bool) string { + if isSelected { + return "→ " + } + return " " + } + + // Checkbox helper function + checked := func(enabled bool) string { + if enabled { + return fmt.Sprintf("\033[32m%s\033[0m", "[✔]") + } + return fmt.Sprintf("\033[31m%s\033[0m", "[✘]") + } + + // Add i18n readme checkbox + s.WriteString(fmt.Sprintf("%sEnable multilingual README: %s \033[33mEnglish is required by default\033[0m\n", + cursor(p.cursor == 4 && !p.languageSection), + checked(p.enableI18nReadme))) + + // Show language selection if i18n is enabled + if p.enableI18nReadme { + s.WriteString("\nLanguages to generate:\n") + for i, choice := range p.languageChoices { + isCurrentCursor := p.languageSection && p.cursor == i + + statusText := "" + if choice.required { + statusText = " \033[33m(required)\033[0m" + } + + s.WriteString(fmt.Sprintf(" %s%s: %s%s%s\n", + cursor(isCurrentCursor), + choice.name, + checked(choice.selected), + statusText, + "\033[0m")) + } + } + + // Add operation hints + s.WriteString("\n\033[36mControls:\033[0m\n") + s.WriteString(" ↑/↓ Navigate • Space/Tab Toggle selection • Enter Next step\n") + if p.warning != "" { - s += fmt.Sprintf("\033[31m%s\033[0m\n", p.warning) + s.WriteString(fmt.Sprintf("\n\033[31m%s\033[0m\n", p.warning)) } - return s + + return s.String() } func (p *profile) checkRule() bool { @@ -91,54 +178,126 @@ func (p profile) Update(msg tea.Msg) (subMenu, subMenuEvent, tea.Cmd) { case "ctrl+c": return p, SUB_MENU_EVENT_NONE, tea.Quit case "down": - // check if empty - if !p.checkRule() { - return p, SUB_MENU_EVENT_NONE, nil - } + if p.languageSection { + // In language selection section + p.cursor++ + if p.cursor >= len(p.languageChoices) { + p.cursor = len(p.languageChoices) - 1 + } + } else { + // In main form section + if p.cursor <= 3 && !p.checkRule() { + return p, SUB_MENU_EVENT_NONE, nil + } - // focus next - p.cursor++ - if p.cursor >= len(p.inputs) { - p.cursor = 0 + p.cursor++ + if p.enableI18nReadme && p.cursor == 5 { + // Move to language selection + p.languageSection = true + p.cursor = 0 + } else if p.cursor > 4 { + p.cursor = 0 + } } case "up": - if !p.checkRule() { - return p, SUB_MENU_EVENT_NONE, nil - } + if p.languageSection { + // In language selection section + p.cursor-- + if p.cursor < 0 { + // Move back to checkbox + p.languageSection = false + p.cursor = 4 + } + } else { + // In main form section + if p.cursor <= 3 && !p.checkRule() { + return p, SUB_MENU_EVENT_NONE, nil + } - p.cursor-- - if p.cursor < 0 { - p.cursor = len(p.inputs) - 1 + p.cursor-- + if p.cursor < 0 { + if p.enableI18nReadme { + // Move to last language option + p.languageSection = true + p.cursor = len(p.languageChoices) - 1 + } else { + p.cursor = 4 + } + } } case "enter": - if !p.checkRule() { + if p.languageSection { + // In language selection, enter means finish + return p, SUB_MENU_EVENT_NEXT, nil + } + + if p.cursor == 4 { + // Toggle checkbox for i18n readme + p.enableI18nReadme = !p.enableI18nReadme + if !p.enableI18nReadme { + // Reset language selections to default when disabled + for i := range p.languageChoices { + p.languageChoices[i].selected = p.languageChoices[i].required + } + } + return p, SUB_MENU_EVENT_NONE, nil + } + + if p.cursor <= 3 && !p.checkRule() { return p, SUB_MENU_EVENT_NONE, nil } // submit - if p.cursor == len(p.inputs)-1 { - return p, SUB_MENU_EVENT_NEXT, nil + if p.cursor == 3 && p.inputs[p.cursor].Value() == "" { + // repo is optional, move to checkbox + p.cursor = 4 + return p, SUB_MENU_EVENT_NONE, nil + } + + if p.cursor == 3 { + p.cursor = 4 + return p, SUB_MENU_EVENT_NONE, nil } // move to next p.cursor++ + case " ", "tab": + if p.languageSection { + // Toggle language selection (but not for required ones) + if !p.languageChoices[p.cursor].required { + p.languageChoices[p.cursor].selected = !p.languageChoices[p.cursor].selected + } + } else if p.cursor == 4 { + // Toggle checkbox with space/tab when focused on it + p.enableI18nReadme = !p.enableI18nReadme + if !p.enableI18nReadme { + // Reset language selections to default when disabled + for i := range p.languageChoices { + p.languageChoices[i].selected = p.languageChoices[i].required + } + } + } } } - // update cursor - for i := 0; i < len(p.inputs); i++ { - if i == p.cursor { - p.inputs[i].Focus() - } else { - p.inputs[i].Blur() + // update cursor (only for input fields) + if !p.languageSection { + for i := 0; i < len(p.inputs); i++ { + if i == p.cursor { + p.inputs[i].Focus() + } else { + p.inputs[i].Blur() + } } } - // update view - for i := range p.inputs { - var cmd tea.Cmd - p.inputs[i], cmd = p.inputs[i].Update(msg) - if cmd != nil { - cmds = append(cmds, cmd) + // update view (only for input fields when not in language section) + if !p.languageSection && p.cursor <= 3 { + for i := range p.inputs { + var cmd tea.Cmd + p.inputs[i], cmd = p.inputs[i].Update(msg) + if cmd != nil { + cmds = append(cmds, cmd) + } } } @@ -156,3 +315,26 @@ func (p *profile) SetAuthor(author string) { func (p *profile) SetName(name string) { p.inputs[0].SetValue(name) } + +func (p *profile) SetEnableI18nReadme(enable bool) { + p.enableI18nReadme = enable +} + +func (p *profile) SetSelectedLanguages(languages []string) { + // Reset all selections except required ones + for i := range p.languageChoices { + if !p.languageChoices[i].required { + p.languageChoices[i].selected = false + } + } + + // Set selected languages + for _, lang := range languages { + for i, choice := range p.languageChoices { + if choice.code == lang { + p.languageChoices[i].selected = true + break + } + } + } +} diff --git a/cmd/commandline/plugin/templates/.env.example b/cmd/commandline/plugin/templates/.env.example index 60358af873..da8d5af49a 100644 --- a/cmd/commandline/plugin/templates/.env.example +++ b/cmd/commandline/plugin/templates/.env.example @@ -1,3 +1,4 @@ INSTALL_METHOD=remote -REMOTE_INSTALL_URL=debug.dify.ai:5003 +REMOTE_INSTALL_URL=debug.dify.ai +REMOTE_INSTALL_PORT=5003 REMOTE_INSTALL_KEY=********-****-****-****-************ diff --git a/cmd/commandline/plugin/templates/icons/agent_dark.svg b/cmd/commandline/plugin/templates/icons/agent_dark.svg new file mode 100644 index 0000000000..0246418889 --- /dev/null +++ b/cmd/commandline/plugin/templates/icons/agent_dark.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmd/commandline/plugin/templates/icons/agent_light.svg b/cmd/commandline/plugin/templates/icons/agent_light.svg new file mode 100644 index 0000000000..3d958c2826 --- /dev/null +++ b/cmd/commandline/plugin/templates/icons/agent_light.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmd/commandline/plugin/templates/icons/datasource_dark.svg b/cmd/commandline/plugin/templates/icons/datasource_dark.svg new file mode 100644 index 0000000000..53b484f3ec --- /dev/null +++ b/cmd/commandline/plugin/templates/icons/datasource_dark.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmd/commandline/plugin/templates/icons/datasource_light.svg b/cmd/commandline/plugin/templates/icons/datasource_light.svg new file mode 100644 index 0000000000..53b484f3ec --- /dev/null +++ b/cmd/commandline/plugin/templates/icons/datasource_light.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmd/commandline/plugin/templates/icons/extension_dark.svg b/cmd/commandline/plugin/templates/icons/extension_dark.svg new file mode 100644 index 0000000000..2c802655ea --- /dev/null +++ b/cmd/commandline/plugin/templates/icons/extension_dark.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmd/commandline/plugin/templates/icons/extension_light.svg b/cmd/commandline/plugin/templates/icons/extension_light.svg new file mode 100644 index 0000000000..3d3fe5d91b --- /dev/null +++ b/cmd/commandline/plugin/templates/icons/extension_light.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmd/commandline/plugin/templates/icons/model_dark.svg b/cmd/commandline/plugin/templates/icons/model_dark.svg new file mode 100644 index 0000000000..5d924c8e96 --- /dev/null +++ b/cmd/commandline/plugin/templates/icons/model_dark.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmd/commandline/plugin/templates/icons/model_light.svg b/cmd/commandline/plugin/templates/icons/model_light.svg new file mode 100644 index 0000000000..5d924c8e96 --- /dev/null +++ b/cmd/commandline/plugin/templates/icons/model_light.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmd/commandline/plugin/templates/icons/tool_dark.svg b/cmd/commandline/plugin/templates/icons/tool_dark.svg new file mode 100644 index 0000000000..75a6cc1b55 --- /dev/null +++ b/cmd/commandline/plugin/templates/icons/tool_dark.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmd/commandline/plugin/templates/icons/tool_light.svg b/cmd/commandline/plugin/templates/icons/tool_light.svg new file mode 100644 index 0000000000..1decb4e024 --- /dev/null +++ b/cmd/commandline/plugin/templates/icons/tool_light.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmd/commandline/plugin/templates/icons/trigger_dark.svg b/cmd/commandline/plugin/templates/icons/trigger_dark.svg new file mode 100644 index 0000000000..aad122684b --- /dev/null +++ b/cmd/commandline/plugin/templates/icons/trigger_dark.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmd/commandline/plugin/templates/icons/trigger_light.svg b/cmd/commandline/plugin/templates/icons/trigger_light.svg new file mode 100644 index 0000000000..ec87bf8f10 --- /dev/null +++ b/cmd/commandline/plugin/templates/icons/trigger_light.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmd/commandline/plugin/templates/python/.difyignore b/cmd/commandline/plugin/templates/python/.difyignore index 4ea917564e..4685c5eb86 100644 --- a/cmd/commandline/plugin/templates/python/.difyignore +++ b/cmd/commandline/plugin/templates/python/.difyignore @@ -177,3 +177,8 @@ cython_debug/ # Windows Thumbs.db + +# Dify plugin packages +# To prevent packaging repetitively +*.difypkg + diff --git a/cmd/commandline/plugin/templates/python/agent_provider.yaml b/cmd/commandline/plugin/templates/python/agent_provider.yaml index 87ad4de177..b476e1171b 100644 --- a/cmd/commandline/plugin/templates/python/agent_provider.yaml +++ b/cmd/commandline/plugin/templates/python/agent_provider.yaml @@ -2,9 +2,9 @@ identity: author: {{ .Author }} name: {{ .PluginName }} label: - en_US: {{ .PluginName | SnakeToCamel }} + en_US: "{{ .PluginName | SnakeToCamel }}" description: - en_US: {{ .PluginName | SnakeToCamel }} + en_US: "{{ .PluginName | SnakeToCamel }}" icon: icon.svg strategies: - strategies/{{ .PluginName }}.yaml diff --git a/cmd/commandline/plugin/templates/python/agent_strategy.yaml b/cmd/commandline/plugin/templates/python/agent_strategy.yaml index 2bac446fa8..1ee833c9a7 100644 --- a/cmd/commandline/plugin/templates/python/agent_strategy.yaml +++ b/cmd/commandline/plugin/templates/python/agent_strategy.yaml @@ -2,9 +2,9 @@ identity: name: {{ .PluginName }} author: {{ .Author }} label: - en_US: {{ .PluginName | SnakeToCamel }} + en_US: "{{ .PluginName | SnakeToCamel }}" description: - en_US: {{ .PluginName | SnakeToCamel }} + en_US: "{{ .PluginName | SnakeToCamel }}" parameters: - name: model type: model-selector diff --git a/cmd/commandline/plugin/templates/python/icon.svg b/cmd/commandline/plugin/templates/python/icon.svg deleted file mode 100644 index 375222f801..0000000000 --- a/cmd/commandline/plugin/templates/python/icon.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - \ No newline at end of file diff --git a/cmd/commandline/plugin/templates/python/model_provider.yaml b/cmd/commandline/plugin/templates/python/model_provider.yaml index 97c826d7b1..8a0cc61aa9 100644 --- a/cmd/commandline/plugin/templates/python/model_provider.yaml +++ b/cmd/commandline/plugin/templates/python/model_provider.yaml @@ -1,20 +1,20 @@ provider: {{ .PluginName }} label: - en_US: {{ .PluginName | SnakeToCamel }} + en_US: "{{ .PluginName | SnakeToCamel }}" description: - en_US: Models provided by {{ .PluginName }}. - zh_Hans: {{ .PluginName | SnakeToCamel }} 提供的模型。 + en_US: "Models provided by {{ .PluginName }}." + zh_Hans: "{{ .PluginName | SnakeToCamel }} 提供的模型。" icon_small: - en_US: icon_s_en.svg + en_US: "icon_s_en.svg" icon_large: - en_US: icon_l_en.svg + en_US: "icon_l_en.svg" background: "#E5E7EB" help: title: - en_US: Get your API Key from {{ .PluginName }} - zh_Hans: 从 {{ .PluginName | SnakeToCamel }} 获取 API Key + en_US: "Get your API Key from {{ .PluginName }}" + zh_Hans: "从 {{ .PluginName | SnakeToCamel }} 获取 API Key" url: - en_US: https://__put_your_url_here__/account/api-keys + en_US: "https://__put_your_url_here__/account/api-keys" supported_model_types: {{- range .SupportedModelTypes }} - {{ . }} diff --git a/cmd/commandline/plugin/templates/python/tool.yaml b/cmd/commandline/plugin/templates/python/tool.yaml index abb8b5637d..308aaa840a 100644 --- a/cmd/commandline/plugin/templates/python/tool.yaml +++ b/cmd/commandline/plugin/templates/python/tool.yaml @@ -1,16 +1,16 @@ identity: - name: {{ .PluginName }} - author: {{ .Author }} + name: "{{ .PluginName }}" + author: "{{ .Author }}" label: - en_US: {{ .PluginName }} - zh_Hans: {{ .PluginName }} - pt_BR: {{ .PluginName }} + en_US: "{{ .PluginName }}" + zh_Hans: "{{ .PluginName }}" + pt_BR: "{{ .PluginName }}" description: human: - en_US: {{ .PluginDescription }} - zh_Hans: {{ .PluginDescription }} - pt_BR: {{ .PluginDescription }} - llm: {{ .PluginDescription }} + en_US: "{{ .PluginDescription }}" + zh_Hans: "{{ .PluginDescription }}" + pt_BR: "{{ .PluginDescription }}" + llm: "{{ .PluginDescription }}" parameters: - name: query type: string @@ -20,10 +20,10 @@ parameters: zh_Hans: 查询语句 pt_BR: Query string human_description: - en_US: {{ .PluginDescription }} - zh_Hans: {{ .PluginDescription }} - pt_BR: {{ .PluginDescription }} - llm_description: {{ .PluginDescription }} + en_US: "{{ .PluginDescription }}" + zh_Hans: "{{ .PluginDescription }}" + pt_BR: "{{ .PluginDescription }}" + llm_description: "{{ .PluginDescription }}" form: llm extra: python: diff --git a/cmd/commandline/plugin/templates/python/tool_provider.py b/cmd/commandline/plugin/templates/python/tool_provider.py index 7e06f3f26b..0ef4712b2d 100644 --- a/cmd/commandline/plugin/templates/python/tool_provider.py +++ b/cmd/commandline/plugin/templates/python/tool_provider.py @@ -5,6 +5,7 @@ class {{ .PluginName | SnakeToCamel }}Provider(ToolProvider): + def _validate_credentials(self, credentials: dict[str, Any]) -> None: try: """ @@ -12,3 +13,41 @@ def _validate_credentials(self, credentials: dict[str, Any]) -> None: """ except Exception as e: raise ToolProviderCredentialValidationError(str(e)) + + ######################################################################################### + # If OAuth is supported, uncomment the following functions. + # Warning: please make sure that the sdk version is 0.4.2 or higher. + ######################################################################################### + # def _oauth_get_authorization_url(self, redirect_uri: str, system_credentials: Mapping[str, Any]) -> str: + # """ + # Generate the authorization URL for {{ .PluginName }} OAuth. + # """ + # try: + # """ + # IMPLEMENT YOUR AUTHORIZATION URL GENERATION HERE + # """ + # except Exception as e: + # raise ToolProviderOAuthError(str(e)) + # return "" + + # def _oauth_get_credentials( + # self, redirect_uri: str, system_credentials: Mapping[str, Any], request: Request + # ) -> Mapping[str, Any]: + # """ + # Exchange code for access_token. + # """ + # try: + # """ + # IMPLEMENT YOUR CREDENTIALS EXCHANGE HERE + # """ + # except Exception as e: + # raise ToolProviderOAuthError(str(e)) + # return dict() + + # def _oauth_refresh_credentials( + # self, redirect_uri: str, system_credentials: Mapping[str, Any], credentials: Mapping[str, Any] + # ) -> OAuthCredentials: + # """ + # Refresh the credentials + # """ + # return OAuthCredentials(credentials=credentials, expires_at=-1) diff --git a/cmd/commandline/plugin/templates/python/tool_provider.yaml b/cmd/commandline/plugin/templates/python/tool_provider.yaml index 898e78dd9c..77533a35b9 100644 --- a/cmd/commandline/plugin/templates/python/tool_provider.yaml +++ b/cmd/commandline/plugin/templates/python/tool_provider.yaml @@ -1,15 +1,58 @@ identity: - author: {{ .Author }} - name: {{ .PluginName }} + author: "{{ .Author }}" + name: "{{ .PluginName }}" label: - en_US: {{ .PluginName }} - zh_Hans: {{ .PluginName }} - pt_BR: {{ .PluginName }} + en_US: "{{ .PluginName }}" + zh_Hans: "{{ .PluginName }}" + pt_BR: "{{ .PluginName }}" description: - en_US: {{ .PluginDescription }} - zh_Hans: {{ .PluginDescription }} - pt_BR: {{ .PluginDescription }} - icon: icon.svg + en_US: "{{ .PluginDescription }}" + zh_Hans: "{{ .PluginDescription }}" + pt_BR: "{{ .PluginDescription }}" + icon: "icon.svg" + +######################################################################################### +# If you want to support OAuth, you can uncomment the following code. +######################################################################################### +# oauth_schema: +# client_schema: +# - name: "client_id" +# type: "secret-input" +# required: true +# url: https://example.com/oauth/authorize +# placeholder: +# en_US: "Please input your Client ID" +# zh_Hans: "请输入你的 Client ID" +# pt_BR: "Insira seu Client ID" +# help: +# en_US: "Client ID is used to authenticate requests to the example.com API." +# zh_Hans: "Client ID 用于认证请求到 example.com API。" +# pt_BR: "Client ID é usado para autenticar solicitações à API do example.com." +# label: +# zh_Hans: "Client ID" +# en_US: "Client ID" +# - name: "client_secret" +# type: "secret-input" +# required: true +# url: https://example.com/oauth/authorize +# placeholder: +# en_US: "Please input your Client Secret" +# zh_Hans: "请输入你的 Client Secret" +# pt_BR: "Insira seu Client Secret" +# help: +# en_US: "Client Secret is used to authenticate requests to the example.com API." +# zh_Hans: "Client Secret 用于认证请求到 example.com API。" +# pt_BR: "Client Secret é usado para autenticar solicitações à API do example.com." +# label: +# zh_Hans: "Client Secret" +# en_US: "Client Secret" +# credentials_schema: +# - name: "access_token" +# type: "secret-input" +# label: +# zh_Hans: "Access Token" +# en_US: "Access Token" + tools: - tools/{{ .PluginName }}.yaml extra: diff --git a/cmd/commandline/plugin/templates/readme/ja_JP.md b/cmd/commandline/plugin/templates/readme/ja_JP.md new file mode 100644 index 0000000000..05ca914668 --- /dev/null +++ b/cmd/commandline/plugin/templates/readme/ja_JP.md @@ -0,0 +1,3 @@ +## プラグイン Readme + +ここに詳細なプラグイン説明ドキュメントを記載してください。 diff --git a/cmd/commandline/plugin/templates/readme/pt_BR.md b/cmd/commandline/plugin/templates/readme/pt_BR.md new file mode 100644 index 0000000000..16cad5edfa --- /dev/null +++ b/cmd/commandline/plugin/templates/readme/pt_BR.md @@ -0,0 +1,3 @@ +## Readme do Plugin + +Por favor, preencha aqui o documento de descrição detalhada do plugin. diff --git a/cmd/commandline/plugin/templates/readme/zh_Hans.md b/cmd/commandline/plugin/templates/readme/zh_Hans.md new file mode 100644 index 0000000000..1a2bfd3d49 --- /dev/null +++ b/cmd/commandline/plugin/templates/readme/zh_Hans.md @@ -0,0 +1,3 @@ +## 插件 Readme + +请在这里填写详细的插件说明文档。 diff --git a/cmd/commandline/signature.go b/cmd/commandline/signature.go index b71e79fcdd..3ccaf3cba0 100644 --- a/cmd/commandline/signature.go +++ b/cmd/commandline/signature.go @@ -2,8 +2,11 @@ package main import ( "os" + "strings" "github.com/langgenius/dify-plugin-daemon/cmd/commandline/signature" + "github.com/langgenius/dify-plugin-daemon/internal/utils/log" + "github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/decoder" "github.com/spf13/cobra" ) @@ -33,7 +36,19 @@ var ( Run: func(c *cobra.Command, args []string) { difypkgPath := args[0] privateKeyPath := c.Flag("private_key").Value.String() - err := signature.Sign(difypkgPath, privateKeyPath) + authorizedCategory := c.Flag("authorized_category").Value.String() + if authorizedCategory != "" { + if !strings.EqualFold(authorizedCategory, string(decoder.AUTHORIZED_CATEGORY_LANGGENIUS)) && + !strings.EqualFold(authorizedCategory, string(decoder.AUTHORIZED_CATEGORY_PARTNER)) && + !strings.EqualFold(authorizedCategory, string(decoder.AUTHORIZED_CATEGORY_COMMUNITY)) { + log.Error("invalid authorized category: %s", authorizedCategory) + os.Exit(1) + } + } + + err := signature.Sign(difypkgPath, privateKeyPath, &decoder.Verification{ + AuthorizedCategory: decoder.AuthorizedCategory(authorizedCategory), + }) if err != nil { os.Exit(1) } @@ -66,5 +81,12 @@ func init() { signatureSignCommand.Flags().StringP("private_key", "p", "", "private key file") signatureSignCommand.MarkFlagRequired("private_key") + signatureSignCommand.Flags().StringP( + "authorized_category", + "c", + string(decoder.AUTHORIZED_CATEGORY_LANGGENIUS), + "authorized category", + ) + signatureVerifyCommand.Flags().StringP("public_key", "p", "", "public key file") } diff --git a/cmd/commandline/signature/sign.go b/cmd/commandline/signature/sign.go index 3a23c83d8a..2302f89cf1 100644 --- a/cmd/commandline/signature/sign.go +++ b/cmd/commandline/signature/sign.go @@ -8,10 +8,11 @@ import ( "github.com/langgenius/dify-plugin-daemon/internal/utils/encryption" "github.com/langgenius/dify-plugin-daemon/internal/utils/log" + "github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/decoder" "github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/signer/withkey" ) -func Sign(difypkgPath string, privateKeyPath string) error { +func Sign(difypkgPath string, privateKeyPath string, verification *decoder.Verification) error { // read the plugin and private key plugin, err := os.ReadFile(difypkgPath) if err != nil { @@ -32,7 +33,7 @@ func Sign(difypkgPath string, privateKeyPath string) error { } // sign the plugin - pluginFile, err := withkey.SignPluginWithPrivateKey(plugin, privateKey) + pluginFile, err := withkey.SignPluginWithPrivateKey(plugin, verification, privateKey) if err != nil { log.Error("Failed to sign plugin: %v", err) return err diff --git a/cmd/commandline/signature/signature_test.go b/cmd/commandline/signature/signature_test.go index 56457916c1..ddf74d0c87 100644 --- a/cmd/commandline/signature/signature_test.go +++ b/cmd/commandline/signature/signature_test.go @@ -7,6 +7,9 @@ import ( "testing" "github.com/langgenius/dify-plugin-daemon/internal/utils/encryption" + "github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/decoder" + "github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/signer" + "github.com/stretchr/testify/assert" ) //go:embed testdata/dummy_plugin.difypkg @@ -77,6 +80,7 @@ func TestSignAndVerify(t *testing.T) { signKeyPath string verifyKeyPath string expectSuccess bool + verification *decoder.Verification } // test cases @@ -86,36 +90,54 @@ func TestSignAndVerify(t *testing.T) { signKeyPath: privateKey1Path, verifyKeyPath: publicKey1Path, expectSuccess: true, + verification: &decoder.Verification{ + AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_LANGGENIUS, + }, }, { name: "sign with keypair1, verify with keypair2", signKeyPath: privateKey1Path, verifyKeyPath: publicKey2Path, expectSuccess: false, + verification: &decoder.Verification{ + AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_LANGGENIUS, + }, }, { name: "sign with keypair2, verify with keypair2", signKeyPath: privateKey2Path, verifyKeyPath: publicKey2Path, expectSuccess: true, + verification: &decoder.Verification{ + AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_LANGGENIUS, + }, }, { name: "sign with keypair2, verify with keypair1", signKeyPath: privateKey2Path, verifyKeyPath: publicKey1Path, expectSuccess: false, + verification: &decoder.Verification{ + AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_LANGGENIUS, + }, }, { name: "sign with keypair1, verify without key", signKeyPath: privateKey1Path, verifyKeyPath: "", expectSuccess: false, + verification: &decoder.Verification{ + AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_LANGGENIUS, + }, }, { name: "sign with keypair2, verify without key", signKeyPath: privateKey2Path, verifyKeyPath: "", expectSuccess: false, + verification: &decoder.Verification{ + AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_LANGGENIUS, + }, }, } @@ -129,7 +151,7 @@ func TestSignAndVerify(t *testing.T) { } // sign the plugin - Sign(testPluginPath, tt.signKeyPath) + Sign(testPluginPath, tt.signKeyPath, tt.verification) // get the path of the signed plugin dir := filepath.Dir(testPluginPath) @@ -150,6 +172,29 @@ func TestSignAndVerify(t *testing.T) { } else if !tt.expectSuccess && err == nil { t.Errorf("Expected verification to fail, but it succeeded") } + + // check the verification + pluginData, err := os.ReadFile(dummyPluginPath) + assert.NoError(t, err) + + decoder, err := decoder.NewZipPluginDecoderWithThirdPartySignatureVerificationConfig( + pluginData, + &decoder.ThirdPartySignatureVerificationConfig{ + Enabled: true, + PublicKeyPaths: []string{tt.verifyKeyPath}, + }, + ) + assert.NoError(t, err) + + if tt.expectSuccess { + verification, err := decoder.Verification() + assert.NoError(t, err) + assert.Equal(t, tt.verification.AuthorizedCategory, verification.AuthorizedCategory) + } else { + verification, err := decoder.Verification() + assert.Error(t, err) + assert.Nil(t, verification) + } }) } } @@ -195,7 +240,9 @@ func TestVerifyTampered(t *testing.T) { publicKeyPath := keyPairName + ".public.pem" // Sign the plugin - Sign(dummyPluginPath, privateKeyPath) + Sign(dummyPluginPath, privateKeyPath, &decoder.Verification{ + AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_LANGGENIUS, + }) // Get the path of the signed plugin dir := filepath.Dir(dummyPluginPath) @@ -225,3 +272,34 @@ func TestVerifyTampered(t *testing.T) { t.Errorf("Expected verification of tampered file to fail, but it succeeded") } } + +/* +Formerly, the plugin is all signed by langgenius but has no authorized category +*/ +func TestVerifyPluginWithoutVerificationField(t *testing.T) { + tempDir := t.TempDir() + + // extract the minimal plugin content from the embedded data to a file + dummyPluginPath := filepath.Join(tempDir, "dummy_plugin.difypkg") + if err := os.WriteFile(dummyPluginPath, dummyPlugin, 0644); err != nil { + t.Fatalf("Failed to create dummy plugin file: %v", err) + } + + pluginPackageWithoutVerificationField, err := signer.TraditionalSignPlugin(dummyPlugin) + if err != nil { + t.Fatalf("Failed to sign plugin: %v", err) + } + + // sign a plugin + decoder, err := decoder.NewZipPluginDecoder( + pluginPackageWithoutVerificationField, + ) + assert.NoError(t, err) + + verification, err := decoder.Verification() + assert.NoError(t, err) + assert.Nil(t, verification) + + verified := decoder.Verified() + assert.True(t, verified) +} diff --git a/cmd/license/sign/main.go b/cmd/license/sign/main.go index e64adbd74e..3db9940cf1 100644 --- a/cmd/license/sign/main.go +++ b/cmd/license/sign/main.go @@ -5,18 +5,21 @@ import ( "os" "github.com/langgenius/dify-plugin-daemon/internal/utils/log" + "github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/decoder" "github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/signer" ) func main() { var ( - in_path string - out_path string - help bool + in_path string + out_path string + help bool + authorized_category string ) flag.StringVar(&in_path, "in", "", "input plugin file path") flag.StringVar(&out_path, "out", "", "output plugin file path") + flag.StringVar(&authorized_category, "authorized_category", "", "authorized category") flag.BoolVar(&help, "help", false, "show help") flag.Parse() @@ -32,7 +35,9 @@ func main() { } // sign plugin - pluginFile, err := signer.SignPlugin(plugin) + pluginFile, err := signer.SignPlugin(plugin, &decoder.Verification{ + AuthorizedCategory: decoder.AuthorizedCategory(authorized_category), + }) if err != nil { log.Panic("failed to sign plugin %v", err) } diff --git a/cmd/tests/main.go b/cmd/tests/main.go index 7905807777..ba22995cc8 100644 --- a/cmd/tests/main.go +++ b/cmd/tests/main.go @@ -1,5 +1,21 @@ package main +import ( + "fmt" + + "github.com/langgenius/dify-plugin-daemon/internal/utils/parser" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/model_entities" +) + func main() { + const data = `{"data":{"structured_output":{"name":"yeuoly","age":0},"model":"gpt-4o-mini","prompt_messages":[],"system_fingerprint":"fp_34a54ae93c","delta":{"index":0,"message":{"role":"assistant","content":"","name":null,"tool_calls":[]},"usage":null,"finish_reason":null}},"error":""}` + type resp struct { + Data model_entities.LLMResultChunkWithStructuredOutput `json:"data"` + } + output, err := parser.UnmarshalJson[resp](data) + if err != nil { + panic(err) + } + fmt.Println(parser.MarshalJson(output.Data)) } diff --git a/docs/runtime/sri.md b/docs/runtime/sri.md new file mode 100644 index 0000000000..7d7e30128e --- /dev/null +++ b/docs/runtime/sri.md @@ -0,0 +1,281 @@ +# Dify Plugin Daemon - Serverless Runtime Interface (SRI) + +The Serverless Runtime Interface (**SRI**) is a set of HTTP APIs for packaging plugins into serverless components, allowing the Dify Plugin Daemon to remotely launch and operate them on external platforms (e.g., AWS Lambda). + +This interface enables the daemon to communicate with remote runtime environments via standard protocols to handle plugin deployment, execution, and instance queries. + +> ⚠️ **Note**: This interface is currently in the **Alpha** stage. Stability and backward compatibility are not guaranteed. A production-grade SRI implementation is available in the enterprise edition. For support, please contact `business@dify.ai`. + +--- + +## 🔧 Basic Configuration + +The daemon is configured using the following environment variables: + +| Variable | Description | +|----------|-------------| +| `DIFY_PLUGIN_SERVERLESS_CONNECTOR_URL` | Base URL of the remote runtime environment, e.g., `https://example.com` | +| `DIFY_PLUGIN_SERVERLESS_CONNECTOR_API_KEY` | Authentication token for accessing SRI, passed in the `Authorization` request header | + +--- + +## 📡 API Endpoints + +### `GET /ping` + +Used by the daemon for connectivity checks during startup. + +**Request** + +```http +GET /ping +Authorization: +``` + +**Response** + +- `200 OK`, response body is plain text: `"pong"` + +--- + +### `GET /v1/runner/instances` + +Returns information about plugin instances that are ready to run. + +**Query Parameters** + +- `filename` (required): Name of the uploaded plugin package, in the format: + + ``` + vendor@plugin@version@hash.difypkg + ``` + +**Response** + +```json +{ + "items": [ + { + "ID": "string", + "Name": "string", + "Endpoint": "string", + "ResourceName": "string" + } + ] +} +``` + +--- + +### `POST /v1/launch` + +Launches a plugin using a streaming event protocol for real-time daemon parsing of startup status. + +> This API uses `multipart/form-data` for submission and returns status via **Server-Sent Events (SSE)**. + +**Request Fields** + +| Field | Type | Description | +|------------|----------|-----------------------------------------------------| +| `context` | file | Plugin package file in `.difypkg` format | +| `verified` | boolean | Whether the plugin has been verified by the daemon | + +**SSE Response Format** + +```json +{ + "Stage": "healthz|start|build|run|end", + "State": "running|success|failed", + "Obj": "string", + "Message": "string" +} +``` + +**Stage Descriptions** + +| Stage | Meaning | Description | +|---------|------------------|--------------------------------------------------| +| healthz | Health check | Initializes runtime resources and containers | +| start | Startup prep | Prepares the environment | +| build | Build phase | Builds plugin dependencies and packages image | +| run | Execution phase | Plugin is running; returns key info on success | +| end | Completion | Final state confirmation: success or failure | + +When a message with `Stage=run` and `State=success` is received, the daemon will extract details and register the plugin instance: + +``` +endpoint=http://...,name=...,id=... +``` + +**Error Handling** + +- If any stage returns `State = failed`, it is considered a launch failure +- The daemon should abort the process and output the `Message` field as the error + +--- + +## 🔁 Communication Sequence (ASCII) + +```text +daemon Serverless Runtime Interface + |-------------------------------------->| + | GET /ping | + |<--------------------------------------| + | 200 OK "pong" | + |-------------------------------------->| + | GET /v1/runner/instances | + | filename | + |<--------------------------------------| + | {items} | + |-------------------------------------->| + | POST /v1/launch | + | context, verified multipart payload | + |<--------------------------------------| + | Building plugin... (SSE) | + |<--------------------------------------| + | Launching plugin... (SSE) | + |<--------------------------------------| + | Function: [Name] (SSE) | + |<--------------------------------------| + | FunctionUrl: [Endpoint] (SSE) | + |<--------------------------------------| + | Done: Plugin launched (SSE) | +``` + +--- + +## 📦 Plugin File Naming Convention + +Plugin files must use the `.difypkg` extension and follow this naming convention: + +``` +@@@.difypkg +``` + +Example: + +``` +langgenius@tavily@0.0.5@7f277f7a63e36b1b3e9ed53e55daab0b281599d14902664bade86215f5374f06.difypkg +``` + +--- + +## 📬 Contact Us + +For access to the enterprise-supported version or more details about plugin packaging and deployment, please contact: + +📧 `business@dify.ai` + +--- + +## 📘 OpenAPI Specification (YAML) + +```yaml +openapi: 3.0.3 +info: + title: Dify Plugin Daemon - Serverless Runtime Interface (SRI) + version: alpha + description: HTTP API specification for the Dify Plugin Daemon's Serverless Runtime + Interface (SRI). +paths: + /ping: + get: + summary: Health check endpoint + description: Used by the daemon to verify connectivity with the SRI. + responses: + '200': + description: Returns 'pong' if the service is alive + content: + text/plain: + schema: + type: string + example: pong + security: + - apiKeyAuth: [] + /v1/runner/instances: + get: + summary: List available plugin instances + parameters: + - name: filename + in: query + required: true + schema: + type: string + description: Full plugin package filename (e.g., vendor@plugin@version@hash.difypkg) + responses: + '200': + description: List of available plugin instances + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + type: object + properties: + ID: + type: string + Name: + type: string + Endpoint: + type: string + ResourceName: + type: string + security: + - apiKeyAuth: [] + /v1/launch: + post: + summary: Launch a plugin via SSE + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + context: + type: string + format: binary + description: Plugin package file (.difypkg) + verified: + type: boolean + description: Whether the plugin is verified + required: + - context + responses: + '200': + description: Server-Sent Events stream with plugin launch stages + content: + text/event-stream: + schema: + type: object + properties: + Stage: + type: string + enum: + - healthz + - start + - build + - run + - end + State: + type: string + enum: + - running + - success + - failed + Obj: + type: string + Message: + type: string + security: + - apiKeyAuth: [] +components: + securitySchemes: + apiKeyAuth: + type: apiKey + in: header + name: Authorization +``` diff --git a/docs/runtime/sri_cn.md b/docs/runtime/sri_cn.md new file mode 100644 index 0000000000..7e5b6b829e --- /dev/null +++ b/docs/runtime/sri_cn.md @@ -0,0 +1,282 @@ +# Dify Plugin Daemon - Serverless Runtime Interface (SRI) + +Serverless Runtime Interface (**SRI**) 是一组用于将插件封装为 Serverless 组件,并由 Dify Plugin Daemon 在外部平台(如 AWS Lambda)上远程启动和运行的 HTTP 接口规范。 + +该接口允许 daemon 通过标准协议与远程运行环境通信,实现插件部署、运行、实例查询等功能。 + +> ⚠️ **注意**:当前接口处于 **Alpha 阶段**,不保证稳定性与向后兼容性。 企业版中提供生产级 SRI 实现, 如需请联系 `business@dify.ai`。 + +--- + +## 🔧 基础配置 + +daemon 通过如下环境变量进行配置: + +| 变量名 | 含义 | +|--------|------| +| `DIFY_PLUGIN_SERVERLESS_CONNECTOR_URL` | 指定远程运行环境的 Base URL,例如 `https://example.com` | +| `DIFY_PLUGIN_SERVERLESS_CONNECTOR_API_KEY` | 用于访问 SRI 的鉴权 token,将被加入请求 Header 中的 `Authorization` 字段 | + +--- + +## 📡 接口说明 + +### `GET /ping` + +用于 daemon 启动时的连通性检查。 + +**请求** + +```http +GET /ping +Authorization: +``` + +**响应** + +- `200 OK`,响应体为纯文本字符串 `"pong"` + +--- + +### `GET /v1/runner/instances` + +返回支持运行的插件实例信息。 + +**请求参数** + +- `filename`(必填):上传的插件包文件名,格式为: + + ``` + vendor@plugin@version@hash.difypkg + ``` + +**响应** + +```json +{ + "items": [ + { + "ID": "string", + "Name": "string", + "Endpoint": "string", + "ResourceName": "string" + } + ] +} +``` + +--- + +### `POST /v1/launch` + +以流式事件的方式启动插件,供 daemon 实时解析启动状态。 + +> 本接口使用 `multipart/form-data` 提交,同时以 **Server-Sent Events(SSE)** 返回插件运行状态流。 + +**请求字段** + +| 字段名 | 类型 | 描述 | +|------------|-----------|----------------------------------------------| +| `context` | file | `.difypkg` 格式的插件包 | +| `verified` | boolean | 插件是否已通过 daemon 验证(由 manifest 判断) | + +**SSE 响应格式** + +```json +{ + "Stage": "healthz|start|build|run|end", + "State": "running|success|failed", + "Obj": "string", + "Message": "string" +} +``` + +**阶段说明** + +| Stage | 含义 | 行为说明 | +|---------|--------------|------------------------------------------------| +| healthz | 健康检查 | 初始化运行时资源,准备插件容器 | +| start | 启动准备阶段 | 准备环境 | +| build | 构建阶段 | 构建插件依赖,打包镜像 | +| run | 运行阶段 | 插件运行中,如成功将返回关键信息 | +| end | 启动完成 | 插件运行结果确认,可能为 success 或 failed | + +当接收到以下格式的 `Stage=run` 且 `State=success` 消息时,daemon 将提取其中信息并建立插件实例: + +``` +endpoint=http://...,name=...,id=... +``` + +**错误处理** + +- 任意阶段返回 `State = failed` 即视为启动失败 +- daemon 应中断流程并抛出异常,输出 `Message` 内容作为错误信息 + +--- + +## 🔁 通信时序图(ASCII) + +```text +daemon Serverless Runtime Interface + |-------------------------------------->| + | GET /ping | + |<--------------------------------------| + | 200 OK "pong" | + |-------------------------------------->| + | GET /v1/runner/instances | + | filename | + |<--------------------------------------| + | {items} | + |-------------------------------------->| + | POST /v1/launch | + | context, verified multipart payload | + |<--------------------------------------| + | Building plugin... (SSE) | + |<--------------------------------------| + | Launching plugin... (SSE) | + |<--------------------------------------| + | Function: [Name] (SSE) | + |<--------------------------------------| + | FunctionUrl: [Endpoint] (SSE) | + |<--------------------------------------| + | Done: Plugin launched (SSE) | +``` + +--- + +## 📦 插件文件名规范 + +插件文件扩展名必须为 `.difypkg`,命名格式如下: + +``` +@@@.difypkg +``` + +示例: + +``` +langgenius@tavily@0.0.5@7f277f7a63e36b1b3e9ed53e55daab0b281599d14902664bade86215f5374f06.difypkg +``` + +--- + +## 📬 联系我们 + +如需接入商业支持版本,或希望深入了解插件打包与部署规范,请联系: + +📧 `business@dify.ai` + +--- + +## 📘 OpenAPI 规范(YAML) + +```yaml +openapi: 3.0.3 +info: + title: Dify Plugin Daemon - Serverless Runtime Interface (SRI) + version: alpha + description: HTTP API specification for the Dify Plugin Daemon's Serverless Runtime + Interface (SRI). +paths: + /ping: + get: + summary: Health check endpoint + description: Used by the daemon to verify connectivity with the SRI. + responses: + '200': + description: Returns 'pong' if the service is alive + content: + text/plain: + schema: + type: string + example: pong + security: + - apiKeyAuth: [] + /v1/runner/instances: + get: + summary: List available plugin instances + parameters: + - name: filename + in: query + required: true + schema: + type: string + description: Full plugin package filename (e.g., vendor@plugin@version@hash.difypkg) + responses: + '200': + description: List of available plugin instances + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + type: object + properties: + ID: + type: string + Name: + type: string + Endpoint: + type: string + ResourceName: + type: string + security: + - apiKeyAuth: [] + /v1/launch: + post: + summary: Launch a plugin via SSE + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + context: + type: string + format: binary + description: Plugin package file (.difypkg) + verified: + type: boolean + description: Whether the plugin is verified + required: + - context + responses: + '200': + description: Server-Sent Events stream with plugin launch stages + content: + text/event-stream: + schema: + type: object + properties: + Stage: + type: string + enum: + - healthz + - start + - build + - run + - end + State: + type: string + enum: + - running + - success + - failed + Obj: + type: string + Message: + type: string + security: + - apiKeyAuth: [] +components: + securitySchemes: + apiKeyAuth: + type: apiKey + in: header + name: Authorization + +``` diff --git a/go.mod b/go.mod index e386f6608e..9102b45b68 100644 --- a/go.mod +++ b/go.mod @@ -1,66 +1,62 @@ module github.com/langgenius/dify-plugin-daemon -go 1.23.0 - -toolchain go1.23.3 +go 1.23.3 require ( - cloud.google.com/go/storage v1.51.0 - github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 - github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible - github.com/aws/aws-sdk-go-v2 v1.30.4 - github.com/aws/aws-sdk-go-v2/config v1.27.31 - github.com/aws/aws-sdk-go-v2/credentials v1.17.30 - github.com/aws/aws-sdk-go-v2/service/s3 v1.60.1 github.com/charmbracelet/bubbles v0.19.0 github.com/charmbracelet/bubbletea v1.1.0 - github.com/fsouza/fake-gcs-server v1.52.2 github.com/fxamacker/cbor/v2 v2.7.0 github.com/getsentry/sentry-go v0.30.0 github.com/go-git/go-git v4.7.0+incompatible github.com/google/uuid v1.6.0 github.com/hashicorp/go-version v1.7.0 + github.com/langgenius/dify-cloud-kit v0.0.0-20250611112407-c54203d9e948 + github.com/panjf2000/ants/v2 v2.10.0 github.com/redis/go-redis/v9 v9.5.5 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 - github.com/tencentyun/cos-go-sdk-v5 v0.7.62 github.com/xeipuuv/gojsonschema v1.2.0 - golang.org/x/oauth2 v0.28.0 - google.golang.org/api v0.224.0 + golang.org/x/tools v0.22.0 gorm.io/driver/mysql v1.5.7 gorm.io/gorm v1.25.11 ) require ( - cel.dev/expr v0.19.2 // indirect - cloud.google.com/go v0.118.3 // indirect - cloud.google.com/go/auth v0.15.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect + cel.dev/expr v0.20.0 // indirect + cloud.google.com/go v0.121.0 // indirect + cloud.google.com/go/auth v0.16.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect - cloud.google.com/go/iam v1.4.1 // indirect + cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/monitoring v1.24.0 // indirect - cloud.google.com/go/pubsub v1.47.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect + cloud.google.com/go/storage v1.54.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect + github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible // indirect github.com/atotto/clipboard v0.1.4 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.16 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.30.5 // indirect - github.com/aws/smithy-go v1.20.4 // indirect + github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect + github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.79.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect + github.com/aws/smithy-go v1.22.2 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/lipgloss v0.13.0 // indirect @@ -75,17 +71,16 @@ require ( github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/renameio/v2 v2.0.0 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.5 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect - github.com/gorilla/handlers v1.5.2 // indirect - github.com/gorilla/mux v1.8.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/huaweicloud/huaweicloud-sdk-go-obs v3.25.4+incompatible // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect @@ -102,8 +97,6 @@ require ( github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.15.2 // indirect - github.com/panjf2000/ants/v2 v2.11.2 // indirect - github.com/pkg/xattr v0.4.10 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect @@ -113,26 +106,32 @@ require ( github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/src-d/gcfg v1.4.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/tencentyun/cos-go-sdk-v5 v0.7.65 // indirect + github.com/volcengine/ve-tos-golang-sdk/v2 v2.7.12 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - go.opencensus.io v0.24.0 // indirect + github.com/zeebo/errs v1.4.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect - go.opentelemetry.io/otel v1.34.0 // indirect - go.opentelemetry.io/otel/metric v1.34.0 // indirect - go.opentelemetry.io/otel/sdk v1.34.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect - go.opentelemetry.io/otel/trace v1.34.0 // indirect - golang.org/x/time v0.10.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.35.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/time v0.11.0 // indirect + google.golang.org/api v0.232.0 // indirect google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/grpc v1.71.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 // indirect + google.golang.org/grpc v1.72.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/src-d/go-billy.v4 v4.3.2 // indirect gopkg.in/src-d/go-git.v4 v4.13.1 // indirect @@ -162,7 +161,6 @@ require ( github.com/mitchellh/mapstructure v1.5.0 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/panjf2000/ants v1.3.0 github.com/panjf2000/gnet/v2 v2.5.5 github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/shopspring/decimal v1.4.0 @@ -172,13 +170,13 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.36.0 // indirect + golang.org/x/crypto v0.38.0 // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 - golang.org/x/net v0.38.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/postgres v1.5.9 diff --git a/go.sum b/go.sum index b0f5b6ddd3..2f09319f46 100644 --- a/go.sum +++ b/go.sum @@ -1,45 +1,39 @@ -cel.dev/expr v0.19.2 h1:V354PbqIXr9IQdwy4SYA4xa0HXaWq1BUPAGzugBY5V4= -cel.dev/expr v0.19.2/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.118.3 h1:jsypSnrE/w4mJysioGdMBg4MiW/hHx/sArFpaBWHdME= -cloud.google.com/go v0.118.3/go.mod h1:Lhs3YLnBlwJ4KA6nuObNMZ/fCbOQBPuWKPoE0Wa/9Vc= -cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= -cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= -cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= -cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= +cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI= +cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cloud.google.com/go v0.121.0 h1:pgfwva8nGw7vivjZiRfrmglGWiCJBP+0OmDpenG/Fwg= +cloud.google.com/go v0.121.0/go.mod h1:rS7Kytwheu/y9buoDmu5EIpMMCI4Mb8ND4aeN4Vwj7Q= +cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= +cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= -cloud.google.com/go/iam v1.4.1 h1:cFC25Nv+u5BkTR/BT1tXdoF2daiVbZ1RLx2eqfQ9RMM= -cloud.google.com/go/iam v1.4.1/go.mod h1:2vUEJpUG3Q9p2UdsyksaKpDzlwOrnMzS30isdReIcLM= -cloud.google.com/go/kms v1.21.0 h1:x3EeWKuYwdlo2HLse/876ZrKjk2L5r7Uexfm8+p6mSI= -cloud.google.com/go/kms v1.21.0/go.mod h1:zoFXMhVVK7lQ3JC9xmhHMoQhnjEDZFoLAr5YMwzBLtk= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= -cloud.google.com/go/longrunning v0.6.5 h1:sD+t8DO8j4HKW4QfouCklg7ZC1qC4uzVZt8iz3uTW+Q= -cloud.google.com/go/longrunning v0.6.5/go.mod h1:Et04XK+0TTLKa5IPYryKf5DkpwImy6TluQ1QTLwlKmY= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= cloud.google.com/go/monitoring v1.24.0 h1:csSKiCJ+WVRgNkRzzz3BPoGjFhjPY23ZTcaenToJxMM= cloud.google.com/go/monitoring v1.24.0/go.mod h1:Bd1PRK5bmQBQNnuGwHBfUamAV1ys9049oEPHnn4pcsc= -cloud.google.com/go/pubsub v1.47.0 h1:Ou2Qu4INnf7ykrFjGv2ntFOjVo8Nloh/+OffF4mUu9w= -cloud.google.com/go/pubsub v1.47.0/go.mod h1:LaENesmga+2u0nDtLkIOILskxsfvn/BXX9Ak1NFxOs8= -cloud.google.com/go/storage v1.51.0 h1:ZVZ11zCiD7b3k+cH5lQs/qcNaoSz3U9I0jgwVzqDlCw= -cloud.google.com/go/storage v1.51.0/go.mod h1:YEJfu/Ki3i5oHC/7jyTgsGZwdQ8P9hqMqvpi5kRKGgc= +cloud.google.com/go/storage v1.54.0 h1:Du3XEyliAiftfyW0bwfdppm2MMLdpVAfiIg4T2nAI+0= +cloud.google.com/go/storage v1.54.0/go.mod h1:hIi9Boe8cHxTyaeqh7KMMwKg088VblFK46C2x/BWaZE= cloud.google.com/go/trace v1.11.3 h1:c+I4YFjxRQjvAhRmSsmjpASUKq88chOX854ied0K/pE= cloud.google.com/go/trace v1.11.3/go.mod h1:pt7zCYiDSQjC9Y2oqCsh9jF4GStB/hmjrYLsxRR27q8= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM= -github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI= -github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ= -github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0 h1:LR0kAX9ykz8G4YgLCaRDVJ3+n43R8MneB5dTy2konZo= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0/go.mod h1:DWAciXemNf++PQJLeXUB4HHH5OpsAh12HZnu2wXE1jA= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 h1:lhZdRq7TIx0GJQvSyX2Si406vrYsov2FXGp/RnSEtcs= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1/go.mod h1:8cl44BDmi+effbARHMQjgOKA2AYvcohNm7KEt42mSV8= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= @@ -54,42 +48,42 @@ github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYU github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aws/aws-sdk-go-v2 v1.30.4 h1:frhcagrVNrzmT95RJImMHgabt99vkXGslubDaDagTk8= -github.com/aws/aws-sdk-go-v2 v1.30.4/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 h1:70PVAiL15/aBMh5LThwgXdSQorVr91L127ttckI9QQU= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4/go.mod h1:/MQxMqci8tlqDH+pjmoLu1i0tbWCUP1hhyMRuFxpQCw= -github.com/aws/aws-sdk-go-v2/config v1.27.31 h1:kxBoRsjhT3pq0cKthgj6RU6bXTm/2SgdoUMyrVw0rAI= -github.com/aws/aws-sdk-go-v2/config v1.27.31/go.mod h1:z04nZdSWFPaDwK3DdJOG2r+scLQzMYuJeW0CujEm9FM= -github.com/aws/aws-sdk-go-v2/credentials v1.17.30 h1:aau/oYFtibVovr2rDt8FHlU17BTicFEMAi29V1U+L5Q= -github.com/aws/aws-sdk-go-v2/credentials v1.17.30/go.mod h1:BPJ/yXV92ZVq6G8uYvbU0gSl8q94UB63nMT5ctNO38g= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 h1:yjwoSyDZF8Jth+mUk5lSPJCkMC0lMy6FaCD51jm6ayE= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12/go.mod h1:fuR57fAgMk7ot3WcNQfb6rSEn+SUffl7ri+aa8uKysI= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 h1:TNyt/+X43KJ9IJJMjKfa3bNTiZbUP7DeCxfbTROESwY= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16/go.mod h1:2DwJF39FlNAUiX5pAc0UNeiz16lK2t7IaFcm0LFHEgc= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 h1:jYfy8UPmd+6kJW5YhY0L1/KftReOGxI/4NtVSTh9O/I= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16/go.mod h1:7ZfEPZxkW42Afq4uQB8H2E2e6ebh6mXTueEpYzjCzcs= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.16 h1:mimdLQkIX1zr8GIPY1ZtALdBQGxcASiBd2MOp8m/dMc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.16/go.mod h1:YHk6owoSwrIsok+cAH9PENCOGoH5PU2EllX4vLtSrsY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18 h1:GckUnpm4EJOAio1c8o25a+b3lVfwVzC9gnSBqiiNmZM= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18/go.mod h1:Br6+bxfG33Dk3ynmkhsW2Z/t9D4+lRqdLDNCKi85w0U= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 h1:tJ5RnkHCiSH0jyd6gROjlJtNwov0eGYNz8s8nFcR0jQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18/go.mod h1:++NHzT+nAF7ZPrHPsA+ENvsXkOO8wEu+C6RXltAG4/c= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16 h1:jg16PhLPUiHIj8zYIW6bqzeQSuHVEiWnGA0Brz5Xv2I= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16/go.mod h1:Uyk1zE1VVdsHSU7096h/rwnXDzOzYQVl+FNPhPw7ShY= -github.com/aws/aws-sdk-go-v2/service/s3 v1.60.1 h1:mx2ucgtv+MWzJesJY9Ig/8AFHgoE5FwLXwUVgW/FGdI= -github.com/aws/aws-sdk-go-v2/service/s3 v1.60.1/go.mod h1:BSPI0EfnYUuNHPS0uqIo5VrRwzie+Fp+YhQOUs16sKI= -github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 h1:zCsFCKvbj25i7p1u94imVoO447I/sFv8qq+lGJhRN0c= -github.com/aws/aws-sdk-go-v2/service/sso v1.22.5/go.mod h1:ZeDX1SnKsVlejeuz41GiajjZpRSWR7/42q/EyA/QEiM= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 h1:SKvPgvdvmiTWoi0GAJ7AsJfOz3ngVkD/ERbs5pUnHNI= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5/go.mod h1:20sz31hv/WsPa3HhU3hfrIet2kxM4Pe0r20eBZ20Tac= -github.com/aws/aws-sdk-go-v2/service/sts v1.30.5 h1:OMsEmCyz2i89XwRwPouAJvhj81wINh+4UK+k/0Yo/q8= -github.com/aws/aws-sdk-go-v2/service/sts v1.30.5/go.mod h1:vmSqFK+BVIwVpDAGZB3CoCXHzurt4qBE8lf+I/kRTh0= -github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4= -github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= +github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14= +github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= +github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= +github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= +github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.2 h1:BCG7DCXEXpNCcpwCxg1oi9pkJWH2+eZzTn9MY56MbVw= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.2/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.79.4 h1:4yxno6bNHkekkfqG/a1nz/gC2gBwhJSojV1+oTE7K+4= +github.com/aws/aws-sdk-go-v2/service/s3 v1.79.4/go.mod h1:qbn305Je/IofWBJ4bJz/Q7pDEtnnoInw/dGt71v6rHE= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= +github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= +github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= @@ -100,7 +94,6 @@ github.com/bytedance/sonic v1.11.9 h1:LFHENlIY/SLzDWverzdOvgMztTxcfcF+cqNsz9pK5z github.com/bytedance/sonic v1.11.9/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbles v0.19.0 h1:gKZkKXPP6GlDk6EcfujDK19PCQqRjaJZQ7QRERx1UF0= @@ -115,35 +108,28 @@ github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4h github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= @@ -155,8 +141,6 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/fsouza/fake-gcs-server v1.52.2 h1:j6ne83nqHrlX5EEor7WWVIKdBsztGtwJ1J2mL+k+iio= -github.com/fsouza/fake-gcs-server v1.52.2/go.mod h1:47HKyIkz6oLTes1R8vEaHLwXfzYsGfmDUk1ViHHAUsA= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= @@ -174,8 +158,8 @@ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxI github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-git/go-git v4.7.0+incompatible h1:+W9rgGY4DOKKdX2x6HxSR7HNeTxqiKrOvKnuittYVdA= github.com/go-git/go-git v4.7.0+incompatible/go.mod h1:6+421e08gnZWn30y26Vchf7efgYLe4dl5OQbBSUXShE= -github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= -github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -193,31 +177,13 @@ github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= @@ -226,28 +192,23 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= -github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= -github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.5 h1:VgzTY2jogw3xt39CusEnFJWm7rlsq5yL5q9XdLOuP5g= -github.com/googleapis/enterprise-certificate-proxy v0.3.5/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= -github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= -github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/huaweicloud/huaweicloud-sdk-go-obs v3.25.4+incompatible h1:yNjwdvn9fwuN6Ouxr0xHM0cVu03YMUWUyFmu2van/Yc= +github.com/huaweicloud/huaweicloud-sdk-go-obs v3.25.4+incompatible/go.mod h1:l7VUhRbTKCzdOacdT4oWCwATKyvZqUOlOqr0Ous3k4s= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -272,13 +233,12 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -288,6 +248,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/langgenius/dify-cloud-kit v0.0.0-20250611112407-c54203d9e948 h1:+NSMZyiXfur8DNA1OIQ5q+NpLEJgiynxFV0q7VFmixc= +github.com/langgenius/dify-cloud-kit v0.0.0-20250611112407-c54203d9e948/go.mod h1:VCtfHs++R61MXdyrfVtPk1VwTM4JHjtY+pYUKO8QdtQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -300,12 +262,6 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/minio/crc64nvme v1.0.0 h1:MeLcBkCTD4pAoU7TciAfwsfxgkhM2u5hCe48hSEVFr0= -github.com/minio/crc64nvme v1.0.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= -github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= -github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.86 h1:DcgQ0AUjLJzRH6y/HrxiZ8CXarA70PAIufXHodP4s+k= -github.com/minio/minio-go/v7 v7.0.86/go.mod h1:VbfO4hYwUu3Of9WqGLBZ8vl3Hxnxo4ngxK4hzQDf4x4= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -323,10 +279,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= -github.com/panjf2000/ants v1.3.0 h1:8pQ+8leaLc9lys2viEEr8md0U4RN6uOSUCE9bOYjQ9M= -github.com/panjf2000/ants v1.3.0/go.mod h1:AaACblRPzq35m1g3enqYcxspbbiOJJYaxU2wMpm1cXY= -github.com/panjf2000/ants/v2 v2.11.2 h1:AVGpMSePxUNpcLaBO34xuIgM1ZdKOiGnpxLXixLi5Jo= -github.com/panjf2000/ants/v2 v2.11.2/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= +github.com/panjf2000/ants/v2 v2.10.0 h1:zhRg1pQUtkyRiOFo2Sbqwjp0GfBNo9cUY2/Grpx1p+8= +github.com/panjf2000/ants/v2 v2.10.0/go.mod h1:7ZxyxsqE4vvW0M7LSD8aI3cKwgFhBHbxnlN8mDqHa1I= github.com/panjf2000/gnet/v2 v2.5.5 h1:H+LqGgCHs2mGJq/4n6YELhMjZ027bNgd5Qb8Wj5nbrM= github.com/panjf2000/gnet/v2 v2.5.5/go.mod h1:ppopMJ8VrDbJu8kDsqFQTgNmpMS8Le5CmPxISf+Sauk= github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= @@ -339,14 +293,11 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjL github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/xattr v0.4.10 h1:Qe0mtiNFHQZ296vRgUjRCoPHPqH7VdTOrZx3g0T+pGA= -github.com/pkg/xattr v0.4.10/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/redis/go-redis/v9 v9.5.5 h1:51VEyMF8eOO+NUHFm8fpg+IOc1xFuFOhxs3R+kPu1FM= github.com/redis/go-redis/v9 v9.5.5/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -354,8 +305,6 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= -github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= @@ -364,6 +313,7 @@ github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWR github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= @@ -376,6 +326,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -388,6 +340,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= @@ -396,14 +349,16 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0= -github.com/tencentyun/cos-go-sdk-v5 v0.7.62 h1:7SZVCc31rkvMxod8nwvG1Ko0N5npT39/s3NhpHBvs70= -github.com/tencentyun/cos-go-sdk-v5 v0.7.62/go.mod h1:8+hG+mQMuRP/OIS9d83syAvXvrMj9HhkND6Q1fLghw0= +github.com/tencentyun/cos-go-sdk-v5 v0.7.65 h1:+WBbfwThfZSbxpf1Dw6fyMwyzVtWBBExqfDJ5giiR2s= +github.com/tencentyun/cos-go-sdk-v5 v0.7.65/go.mod h1:8+hG+mQMuRP/OIS9d83syAvXvrMj9HhkND6Q1fLghw0= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/volcengine/ve-tos-golang-sdk/v2 v2.7.12 h1:u9+32DXQIOFPG8oQ3xrjSAUSyAcaq5bqO4cEBom/6lA= +github.com/volcengine/ve-tos-golang-sdk/v2 v2.7.12/go.mod h1:IrjK84IJJTuOZOTMv/P18Ydjy/x+ow7fF7q11jAxXLM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= @@ -413,30 +368,28 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -go.einride.tech/aip v0.68.1 h1:16/AfSxcQISGN5z9C5lM+0mLYXihrHbQ1onvYTr93aQ= -go.einride.tech/aip v0.68.1/go.mod h1:XaFtaj4HuA3Zwk9xoBtTWgNubZ0ZZXv9BZJCkuKuWbg= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= -go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= +go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -449,89 +402,57 @@ golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= -golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.224.0 h1:Ir4UPtDsNiwIOHdExr3fAj4xZ42QjK7uQte3lORLJwU= -google.golang.org/api v0.224.0/go.mod h1:3V39my2xAGkodXy0vEqcEtkqgw2GtrFL5WuBZlCTCOQ= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/api v0.232.0 h1:qGnmaIMf7KcuwHOlF3mERVzChloDYwRfOJOrHt8YC3I= +google.golang.org/api v0.232.0/go.mod h1:p9QCfBWZk1IJETUdbTKloR5ToFdKbYh2fkjsUL6vNoY= google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0= +google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 h1:IqsN8hx+lWLqlN+Sc3DoMy/watjofWiU8sRFgQ8fhKM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= +google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -557,7 +478,5 @@ gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkw gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/core/dify_invocation/invcation.go b/internal/core/dify_invocation/invcation.go index e3bf5ee4a3..dd5a0f54ca 100644 --- a/internal/core/dify_invocation/invcation.go +++ b/internal/core/dify_invocation/invcation.go @@ -9,6 +9,9 @@ import ( type BackwardsInvocation interface { // InvokeLLM InvokeLLM(payload *InvokeLLMRequest) (*stream.Stream[model_entities.LLMResultChunk], error) + // InvokeLLMWithStructuredOutput + InvokeLLMWithStructuredOutput(payload *InvokeLLMWithStructuredOutputRequest) ( + *stream.Stream[model_entities.LLMResultChunkWithStructuredOutput], error) // InvokeTextEmbedding InvokeTextEmbedding(payload *InvokeTextEmbeddingRequest) (*model_entities.TextEmbeddingResult, error) // InvokeRerank diff --git a/internal/core/dify_invocation/real/http_request.go b/internal/core/dify_invocation/real/http_request.go index 40e02b86b7..06871bd940 100644 --- a/internal/core/dify_invocation/real/http_request.go +++ b/internal/core/dify_invocation/real/http_request.go @@ -57,9 +57,15 @@ func StreamResponse[T any](i *RealBackwardsInvocation, method string, path strin }), http_requests.HttpWriteTimeout(i.writeTimeout), http_requests.HttpReadTimeout(i.readTimeout), + http_requests.HttpUsingLengthPrefixed(true), ) - response, err := http_requests.RequestAndParseStream[BaseBackwardsInvocationResponse[T]](i.client, i.difyPath(path), method, options...) + response, err := http_requests.RequestAndParseStream[BaseBackwardsInvocationResponse[T]]( + i.client, + i.difyPath(path), + method, + options..., + ) if err != nil { return nil, err } @@ -109,6 +115,12 @@ func (i *RealBackwardsInvocation) InvokeLLM(payload *dify_invocation.InvokeLLMRe return StreamResponse[model_entities.LLMResultChunk](i, "POST", "invoke/llm", http_requests.HttpPayloadJson(payload)) } +func (i *RealBackwardsInvocation) InvokeLLMWithStructuredOutput(payload *dify_invocation.InvokeLLMWithStructuredOutputRequest) ( + *stream.Stream[model_entities.LLMResultChunkWithStructuredOutput], error, +) { + return StreamResponse[model_entities.LLMResultChunkWithStructuredOutput](i, "POST", "/invoke/llm/structured-output", http_requests.HttpPayloadJson(payload)) +} + func (i *RealBackwardsInvocation) InvokeTextEmbedding(payload *dify_invocation.InvokeTextEmbeddingRequest) (*model_entities.TextEmbeddingResult, error) { return Request[model_entities.TextEmbeddingResult](i, "POST", "invoke/text-embedding", http_requests.HttpPayloadJson(payload)) } diff --git a/internal/core/dify_invocation/tester/mock.go b/internal/core/dify_invocation/tester/mock.go index 10b7ee3683..2475c16b0a 100644 --- a/internal/core/dify_invocation/tester/mock.go +++ b/internal/core/dify_invocation/tester/mock.go @@ -136,6 +136,62 @@ func (m *MockedDifyInvocation) InvokeLLM(payload *dify_invocation.InvokeLLMReque return stream, nil } +func (m *MockedDifyInvocation) InvokeLLMWithStructuredOutput(payload *dify_invocation.InvokeLLMWithStructuredOutputRequest) ( + *stream.Stream[model_entities.LLMResultChunkWithStructuredOutput], error, +) { + // generate json from payload.StructuredOutputSchema + structuredOutput, err := jsonschema.GenerateValidateJson(payload.StructuredOutputSchema) + if err != nil { + return nil, err + } + + // marshal jsonSchema to string + structuredOutputString := parser.MarshalJson(structuredOutput) + + // split structuredOutputString into 10 parts and write them to the stream + parts := []string{} + for i := 0; i < 10; i++ { + start := i * len(structuredOutputString) / 10 + end := (i + 1) * len(structuredOutputString) / 10 + if i == 9 { // last part + end = len(structuredOutputString) + } + parts = append(parts, structuredOutputString[start:end]) + } + + stream := stream.NewStream[model_entities.LLMResultChunkWithStructuredOutput](11) + routine.Submit(nil, func() { + for i, part := range parts { + stream.Write(model_entities.LLMResultChunkWithStructuredOutput{ + Model: model_entities.LLMModel(payload.Model), + SystemFingerprint: "test", + Delta: model_entities.LLMResultChunkDelta{ + Index: &[]int{i}[0], + Message: model_entities.PromptMessage{ + Role: model_entities.PROMPT_MESSAGE_ROLE_ASSISTANT, + Content: part, + Name: "test", + ToolCalls: []model_entities.PromptMessageToolCall{}, + }, + }, + }) + } + // write the last part + stream.Write(model_entities.LLMResultChunkWithStructuredOutput{ + Model: model_entities.LLMModel(payload.Model), + SystemFingerprint: "test", + Delta: model_entities.LLMResultChunkDelta{ + Index: &[]int{10}[0], + }, + LLMStructuredOutput: model_entities.LLMStructuredOutput{ + StructuredOutput: structuredOutput, + }, + }) + stream.Close() + }) + return stream, nil +} + func (m *MockedDifyInvocation) InvokeTextEmbedding(payload *dify_invocation.InvokeTextEmbeddingRequest) (*model_entities.TextEmbeddingResult, error) { result := model_entities.TextEmbeddingResult{ Model: payload.Model, diff --git a/internal/core/dify_invocation/types.go b/internal/core/dify_invocation/types.go index b08051a7d1..141e6bf897 100644 --- a/internal/core/dify_invocation/types.go +++ b/internal/core/dify_invocation/types.go @@ -18,6 +18,7 @@ type InvokeType string const ( INVOKE_TYPE_LLM InvokeType = "llm" + INVOKE_TYPE_LLM_STRUCTURED_OUTPUT InvokeType = "llm_structured_output" INVOKE_TYPE_TEXT_EMBEDDING InvokeType = "text_embedding" INVOKE_TYPE_RERANK InvokeType = "rerank" INVOKE_TYPE_TTS InvokeType = "tts" @@ -51,6 +52,15 @@ type InvokeLLMRequest struct { InvokeLLMSchema } +type InvokeLLMWithStructuredOutputRequest struct { + BaseInvokeDifyRequest + requests.BaseRequestInvokeModel + // requests.InvokeLLMSchema + // TODO: as completion_params in requests.InvokeLLMSchema is "model_parameters", we declare another one here + InvokeLLMSchema + StructuredOutputSchema map[string]any `json:"structured_output_schema" validate:"required"` +} + type InvokeTextEmbeddingRequest struct { BaseInvokeDifyRequest requests.BaseRequestInvokeModel @@ -220,7 +230,9 @@ func (r *InvokeEncryptRequest) EncryptRequired(settings map[string]any) bool { type InvokeToolRequest struct { BaseInvokeDifyRequest - ToolType requests.ToolType `json:"tool_type" validate:"required,tool_type"` + ToolType requests.ToolType `json:"tool_type" validate:"required,tool_type"` + CredentialId string `json:"credential_id" validate:"omitempty"` + CredentialType string `json:"credential_type" validate:"omitempty"` requests.InvokeToolSchema } diff --git a/internal/core/persistence/init.go b/internal/core/persistence/init.go index 7d4749c1d7..ea21d9000f 100644 --- a/internal/core/persistence/init.go +++ b/internal/core/persistence/init.go @@ -1,7 +1,8 @@ package persistence import ( - "github.com/langgenius/dify-plugin-daemon/internal/oss" + "github.com/langgenius/dify-cloud-kit/oss" + "github.com/langgenius/dify-plugin-daemon/internal/types/app" "github.com/langgenius/dify-plugin-daemon/internal/utils/log" ) diff --git a/internal/core/persistence/persistence_test.go b/internal/core/persistence/persistence_test.go index ff289df07b..05793f0988 100644 --- a/internal/core/persistence/persistence_test.go +++ b/internal/core/persistence/persistence_test.go @@ -4,8 +4,9 @@ import ( "encoding/hex" "testing" + cloudoss "github.com/langgenius/dify-cloud-kit/oss" + "github.com/langgenius/dify-cloud-kit/oss/factory" "github.com/langgenius/dify-plugin-daemon/internal/db" - "github.com/langgenius/dify-plugin-daemon/internal/oss/local" "github.com/langgenius/dify-plugin-daemon/internal/types/app" "github.com/langgenius/dify-plugin-daemon/internal/utils/cache" "github.com/langgenius/dify-plugin-daemon/internal/utils/strings" @@ -31,7 +32,15 @@ func TestPersistenceStoreAndLoad(t *testing.T) { }) defer db.Close() - oss := local.NewLocalStorage("./storage") + oss, err := factory.Load("local", cloudoss.OSSArgs{ + Local: &cloudoss.Local{ + Path: "./storage", + }, + }, + ) + if err != nil { + t.Error("failed to load local storage", err.Error()) + } InitPersistence(oss, &app.Config{ PersistenceStoragePath: "./persistence_storage", @@ -75,7 +84,14 @@ func TestPersistenceSaveAndLoadWithLongKey(t *testing.T) { }) defer db.Close() - InitPersistence(local.NewLocalStorage("./storage"), &app.Config{ + oss, err := factory.Load("local", cloudoss.OSSArgs{ + Local: &cloudoss.Local{ + Path: "./storage", + }, + }) + assert.Nil(t, err) + + InitPersistence(oss, &app.Config{ PersistenceStoragePath: "./persistence_storage", PersistenceStorageMaxSize: 1024 * 1024 * 1024, }) @@ -102,7 +118,12 @@ func TestPersistenceDelete(t *testing.T) { }) defer db.Close() - oss := local.NewLocalStorage("./storage") + oss, err := factory.Load("local", cloudoss.OSSArgs{ + Local: &cloudoss.Local{ + Path: "./storage", + }, + }) + assert.Nil(t, err) InitPersistence(oss, &app.Config{ PersistenceStoragePath: "./persistence_storage", @@ -148,7 +169,12 @@ func TestPersistencePathTraversal(t *testing.T) { }) defer db.Close() - oss := local.NewLocalStorage("./storage") + oss, err := factory.Load("local", cloudoss.OSSArgs{ + Local: &cloudoss.Local{ + Path: "./storage", + }, + }) + assert.Nil(t, err) InitPersistence(oss, &app.Config{ PersistenceStoragePath: "./persistence_storage", diff --git a/internal/core/persistence/wrapper.go b/internal/core/persistence/wrapper.go index e2705c275c..a9d951e1a4 100644 --- a/internal/core/persistence/wrapper.go +++ b/internal/core/persistence/wrapper.go @@ -3,7 +3,7 @@ package persistence import ( "path" - "github.com/langgenius/dify-plugin-daemon/internal/oss" + "github.com/langgenius/dify-cloud-kit/oss" ) type wrapper struct { diff --git a/internal/core/plugin_daemon/access_types/access.go b/internal/core/plugin_daemon/access_types/access.go index 30fdd09967..5b6d526b46 100644 --- a/internal/core/plugin_daemon/access_types/access.go +++ b/internal/core/plugin_daemon/access_types/access.go @@ -3,11 +3,12 @@ package access_types type PluginAccessType string const ( - PLUGIN_ACCESS_TYPE_TOOL PluginAccessType = "tool" - PLUGIN_ACCESS_TYPE_MODEL PluginAccessType = "model" - PLUGIN_ACCESS_TYPE_ENDPOINT PluginAccessType = "endpoint" - PLUGIN_ACCESS_TYPE_AGENT_STRATEGY PluginAccessType = "agent_strategy" - PLUGIN_ACCESS_TYPE_OAUTH PluginAccessType = "oauth" + PLUGIN_ACCESS_TYPE_TOOL PluginAccessType = "tool" + PLUGIN_ACCESS_TYPE_MODEL PluginAccessType = "model" + PLUGIN_ACCESS_TYPE_ENDPOINT PluginAccessType = "endpoint" + PLUGIN_ACCESS_TYPE_AGENT_STRATEGY PluginAccessType = "agent_strategy" + PLUGIN_ACCESS_TYPE_OAUTH PluginAccessType = "oauth" + PLUGIN_ACCESS_TYPE_DYNAMIC_PARAMETER PluginAccessType = "dynamic_parameter" ) func (p PluginAccessType) IsValid() bool { @@ -15,31 +16,34 @@ func (p PluginAccessType) IsValid() bool { p == PLUGIN_ACCESS_TYPE_MODEL || p == PLUGIN_ACCESS_TYPE_ENDPOINT || p == PLUGIN_ACCESS_TYPE_AGENT_STRATEGY || - p == PLUGIN_ACCESS_TYPE_OAUTH + p == PLUGIN_ACCESS_TYPE_OAUTH || + p == PLUGIN_ACCESS_TYPE_DYNAMIC_PARAMETER } type PluginAccessAction string const ( - PLUGIN_ACCESS_ACTION_INVOKE_TOOL PluginAccessAction = "invoke_tool" - PLUGIN_ACCESS_ACTION_VALIDATE_TOOL_CREDENTIALS PluginAccessAction = "validate_tool_credentials" - PLUGIN_ACCESS_ACTION_GET_TOOL_RUNTIME_PARAMETERS PluginAccessAction = "get_tool_runtime_parameters" - PLUGIN_ACCESS_ACTION_INVOKE_LLM PluginAccessAction = "invoke_llm" - PLUGIN_ACCESS_ACTION_INVOKE_TEXT_EMBEDDING PluginAccessAction = "invoke_text_embedding" - PLUGIN_ACCESS_ACTION_INVOKE_RERANK PluginAccessAction = "invoke_rerank" - PLUGIN_ACCESS_ACTION_INVOKE_TTS PluginAccessAction = "invoke_tts" - PLUGIN_ACCESS_ACTION_INVOKE_SPEECH2TEXT PluginAccessAction = "invoke_speech2text" - PLUGIN_ACCESS_ACTION_INVOKE_MODERATION PluginAccessAction = "invoke_moderation" - PLUGIN_ACCESS_ACTION_VALIDATE_PROVIDER_CREDENTIALS PluginAccessAction = "validate_provider_credentials" - PLUGIN_ACCESS_ACTION_VALIDATE_MODEL_CREDENTIALS PluginAccessAction = "validate_model_credentials" - PLUGIN_ACCESS_ACTION_INVOKE_ENDPOINT PluginAccessAction = "invoke_endpoint" - PLUGIN_ACCESS_ACTION_GET_TTS_MODEL_VOICES PluginAccessAction = "get_tts_model_voices" - PLUGIN_ACCESS_ACTION_GET_TEXT_EMBEDDING_NUM_TOKENS PluginAccessAction = "get_text_embedding_num_tokens" - PLUGIN_ACCESS_ACTION_GET_AI_MODEL_SCHEMAS PluginAccessAction = "get_ai_model_schemas" - PLUGIN_ACCESS_ACTION_GET_LLM_NUM_TOKENS PluginAccessAction = "get_llm_num_tokens" - PLUGIN_ACCESS_ACTION_INVOKE_AGENT_STRATEGY PluginAccessAction = "invoke_agent_strategy" - PLUGIN_ACCESS_ACTION_GET_AUTHORIZATION_URL PluginAccessAction = "get_authorization_url" - PLUGIN_ACCESS_ACTION_GET_CREDENTIALS PluginAccessAction = "get_credentials" + PLUGIN_ACCESS_ACTION_INVOKE_TOOL PluginAccessAction = "invoke_tool" + PLUGIN_ACCESS_ACTION_VALIDATE_TOOL_CREDENTIALS PluginAccessAction = "validate_tool_credentials" + PLUGIN_ACCESS_ACTION_GET_TOOL_RUNTIME_PARAMETERS PluginAccessAction = "get_tool_runtime_parameters" + PLUGIN_ACCESS_ACTION_INVOKE_LLM PluginAccessAction = "invoke_llm" + PLUGIN_ACCESS_ACTION_INVOKE_TEXT_EMBEDDING PluginAccessAction = "invoke_text_embedding" + PLUGIN_ACCESS_ACTION_INVOKE_RERANK PluginAccessAction = "invoke_rerank" + PLUGIN_ACCESS_ACTION_INVOKE_TTS PluginAccessAction = "invoke_tts" + PLUGIN_ACCESS_ACTION_INVOKE_SPEECH2TEXT PluginAccessAction = "invoke_speech2text" + PLUGIN_ACCESS_ACTION_INVOKE_MODERATION PluginAccessAction = "invoke_moderation" + PLUGIN_ACCESS_ACTION_VALIDATE_PROVIDER_CREDENTIALS PluginAccessAction = "validate_provider_credentials" + PLUGIN_ACCESS_ACTION_VALIDATE_MODEL_CREDENTIALS PluginAccessAction = "validate_model_credentials" + PLUGIN_ACCESS_ACTION_INVOKE_ENDPOINT PluginAccessAction = "invoke_endpoint" + PLUGIN_ACCESS_ACTION_GET_TTS_MODEL_VOICES PluginAccessAction = "get_tts_model_voices" + PLUGIN_ACCESS_ACTION_GET_TEXT_EMBEDDING_NUM_TOKENS PluginAccessAction = "get_text_embedding_num_tokens" + PLUGIN_ACCESS_ACTION_GET_AI_MODEL_SCHEMAS PluginAccessAction = "get_ai_model_schemas" + PLUGIN_ACCESS_ACTION_GET_LLM_NUM_TOKENS PluginAccessAction = "get_llm_num_tokens" + PLUGIN_ACCESS_ACTION_INVOKE_AGENT_STRATEGY PluginAccessAction = "invoke_agent_strategy" + PLUGIN_ACCESS_ACTION_GET_AUTHORIZATION_URL PluginAccessAction = "get_authorization_url" + PLUGIN_ACCESS_ACTION_GET_CREDENTIALS PluginAccessAction = "get_credentials" + PLUGIN_ACCESS_ACTION_REFRESH_CREDENTIALS PluginAccessAction = "refresh_credentials" + PLUGIN_ACCESS_ACTION_DYNAMIC_PARAMETER_FETCH_OPTIONS PluginAccessAction = "fetch_parameter_options" ) func (p PluginAccessAction) IsValid() bool { @@ -61,5 +65,7 @@ func (p PluginAccessAction) IsValid() bool { p == PLUGIN_ACCESS_ACTION_GET_LLM_NUM_TOKENS || p == PLUGIN_ACCESS_ACTION_INVOKE_AGENT_STRATEGY || p == PLUGIN_ACCESS_ACTION_GET_AUTHORIZATION_URL || - p == PLUGIN_ACCESS_ACTION_GET_CREDENTIALS + p == PLUGIN_ACCESS_ACTION_GET_CREDENTIALS || + p == PLUGIN_ACCESS_ACTION_REFRESH_CREDENTIALS || + p == PLUGIN_ACCESS_ACTION_DYNAMIC_PARAMETER_FETCH_OPTIONS } diff --git a/internal/core/plugin_daemon/backwards_invocation/task.go b/internal/core/plugin_daemon/backwards_invocation/task.go index 4a64f47fb5..7f1efbaacd 100644 --- a/internal/core/plugin_daemon/backwards_invocation/task.go +++ b/internal/core/plugin_daemon/backwards_invocation/task.go @@ -154,6 +154,12 @@ var ( }, "error": "permission denied, you need to enable app access in plugin manifest", }, + dify_invocation.INVOKE_TYPE_LLM_STRUCTURED_OUTPUT: { + "func": func(declaration *plugin_entities.PluginDeclaration) bool { + return declaration.Resource.Permission.AllowInvokeLLM() + }, + "error": "permission denied, you need to enable llm access in plugin manifest", + }, } ) @@ -250,6 +256,9 @@ var ( dify_invocation.INVOKE_TYPE_FETCH_APP: func(handle *BackwardsInvocation) { genericDispatchTask(handle, executeDifyInvocationFetchAppTask) }, + dify_invocation.INVOKE_TYPE_LLM_STRUCTURED_OUTPUT: func(handle *BackwardsInvocation) { + genericDispatchTask(handle, executeDifyInvocationLLMStructuredOutputTask) + }, } ) @@ -337,6 +346,26 @@ func executeDifyInvocationLLMTask( } } +func executeDifyInvocationLLMStructuredOutputTask( + handle *BackwardsInvocation, + request *dify_invocation.InvokeLLMWithStructuredOutputRequest, +) { + response, err := handle.backwardsInvocation.InvokeLLMWithStructuredOutput(request) + if err != nil { + handle.WriteError(fmt.Errorf("invoke llm with structured output model failed: %s", err.Error())) + return + } + + for response.Next() { + value, err := response.Read() + if err != nil { + handle.WriteError(fmt.Errorf("read llm with structured output model failed: %s", err.Error())) + return + } + handle.WriteResponse("stream", value) + } +} + func executeDifyInvocationTextEmbeddingTask( handle *BackwardsInvocation, request *dify_invocation.InvokeTextEmbeddingRequest, diff --git a/internal/core/plugin_daemon/dynamic_select.gen.go b/internal/core/plugin_daemon/dynamic_select.gen.go new file mode 100644 index 0000000000..077752aeba --- /dev/null +++ b/internal/core/plugin_daemon/dynamic_select.gen.go @@ -0,0 +1,23 @@ +// Code generated by controller generator. DO NOT EDIT. + +package plugin_daemon + +import ( + "github.com/langgenius/dify-plugin-daemon/internal/core/session_manager" + "github.com/langgenius/dify-plugin-daemon/internal/utils/stream" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/dynamic_select_entities" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/requests" +) + +func FetchDynamicParameterOptions( + session *session_manager.Session, + request *requests.RequestDynamicParameterSelect, +) ( + *stream.Stream[dynamic_select_entities.DynamicSelectResult], error, +) { + return GenericInvokePlugin[requests.RequestDynamicParameterSelect, dynamic_select_entities.DynamicSelectResult]( + session, + request, + 1, + ) +} diff --git a/internal/core/plugin_daemon/generic.go b/internal/core/plugin_daemon/generic.go index 94ebf0dc04..d8e515af12 100644 --- a/internal/core/plugin_daemon/generic.go +++ b/internal/core/plugin_daemon/generic.go @@ -37,7 +37,7 @@ func GenericInvokePlugin[Req any, Rsp any]( response.Close() return } else { - response.Write(chunk) + response.WriteBlocking(chunk) } case plugin_entities.SESSION_MESSAGE_TYPE_INVOKE: // check if the request contains a aws_event_id diff --git a/internal/core/plugin_daemon/model_service.go b/internal/core/plugin_daemon/model.gen.go similarity index 98% rename from internal/core/plugin_daemon/model_service.go rename to internal/core/plugin_daemon/model.gen.go index 71fd15cfc6..5b881632b5 100644 --- a/internal/core/plugin_daemon/model_service.go +++ b/internal/core/plugin_daemon/model.gen.go @@ -1,3 +1,5 @@ +// Code generated by controller generator. DO NOT EDIT. + package plugin_daemon import ( @@ -20,6 +22,19 @@ func InvokeLLM( ) } +func GetLLMNumTokens( + session *session_manager.Session, + request *requests.RequestGetLLMNumTokens, +) ( + *stream.Stream[model_entities.LLMGetNumTokensResponse], error, +) { + return GenericInvokePlugin[requests.RequestGetLLMNumTokens, model_entities.LLMGetNumTokensResponse]( + session, + request, + 1, + ) +} + func InvokeTextEmbedding( session *session_manager.Session, request *requests.RequestInvokeTextEmbedding, @@ -33,6 +48,19 @@ func InvokeTextEmbedding( ) } +func GetTextEmbeddingNumTokens( + session *session_manager.Session, + request *requests.RequestGetTextEmbeddingNumTokens, +) ( + *stream.Stream[model_entities.GetTextEmbeddingNumTokensResponse], error, +) { + return GenericInvokePlugin[requests.RequestGetTextEmbeddingNumTokens, model_entities.GetTextEmbeddingNumTokensResponse]( + session, + request, + 1, + ) +} + func InvokeRerank( session *session_manager.Session, request *requests.RequestInvokeRerank, @@ -59,6 +87,19 @@ func InvokeTTS( ) } +func GetTTSModelVoices( + session *session_manager.Session, + request *requests.RequestGetTTSModelVoices, +) ( + *stream.Stream[model_entities.GetTTSVoicesResponse], error, +) { + return GenericInvokePlugin[requests.RequestGetTTSModelVoices, model_entities.GetTTSVoicesResponse]( + session, + request, + 1, + ) +} + func InvokeSpeech2Text( session *session_manager.Session, request *requests.RequestInvokeSpeech2Text, @@ -111,45 +152,6 @@ func ValidateModelCredentials( ) } -func GetTTSModelVoices( - session *session_manager.Session, - request *requests.RequestGetTTSModelVoices, -) ( - *stream.Stream[model_entities.GetTTSVoicesResponse], error, -) { - return GenericInvokePlugin[requests.RequestGetTTSModelVoices, model_entities.GetTTSVoicesResponse]( - session, - request, - 1, - ) -} - -func GetTextEmbeddingNumTokens( - session *session_manager.Session, - request *requests.RequestGetTextEmbeddingNumTokens, -) ( - *stream.Stream[model_entities.GetTextEmbeddingNumTokensResponse], error, -) { - return GenericInvokePlugin[requests.RequestGetTextEmbeddingNumTokens, model_entities.GetTextEmbeddingNumTokensResponse]( - session, - request, - 1, - ) -} - -func GetLLMNumTokens( - session *session_manager.Session, - request *requests.RequestGetLLMNumTokens, -) ( - *stream.Stream[model_entities.LLMGetNumTokensResponse], error, -) { - return GenericInvokePlugin[requests.RequestGetLLMNumTokens, model_entities.LLMGetNumTokensResponse]( - session, - request, - 1, - ) -} - func GetAIModelSchema( session *session_manager.Session, request *requests.RequestGetAIModelSchema, diff --git a/internal/core/plugin_daemon/oauth.gen.go b/internal/core/plugin_daemon/oauth.gen.go new file mode 100644 index 0000000000..2ea67bf7dc --- /dev/null +++ b/internal/core/plugin_daemon/oauth.gen.go @@ -0,0 +1,49 @@ +// Code generated by controller generator. DO NOT EDIT. + +package plugin_daemon + +import ( + "github.com/langgenius/dify-plugin-daemon/internal/core/session_manager" + "github.com/langgenius/dify-plugin-daemon/internal/utils/stream" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/oauth_entities" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/requests" +) + +func GetAuthorizationURL( + session *session_manager.Session, + request *requests.RequestOAuthGetAuthorizationURL, +) ( + *stream.Stream[oauth_entities.OAuthGetAuthorizationURLResult], error, +) { + return GenericInvokePlugin[requests.RequestOAuthGetAuthorizationURL, oauth_entities.OAuthGetAuthorizationURLResult]( + session, + request, + 1, + ) +} + +func GetCredentials( + session *session_manager.Session, + request *requests.RequestOAuthGetCredentials, +) ( + *stream.Stream[oauth_entities.OAuthGetCredentialsResult], error, +) { + return GenericInvokePlugin[requests.RequestOAuthGetCredentials, oauth_entities.OAuthGetCredentialsResult]( + session, + request, + 1, + ) +} + +func RefreshCredentials( + session *session_manager.Session, + request *requests.RequestOAuthRefreshCredentials, +) ( + *stream.Stream[oauth_entities.OAuthRefreshCredentialsResult], error, +) { + return GenericInvokePlugin[requests.RequestOAuthRefreshCredentials, oauth_entities.OAuthRefreshCredentialsResult]( + session, + request, + 1, + ) +} diff --git a/internal/core/plugin_daemon/tool.gen.go b/internal/core/plugin_daemon/tool.gen.go new file mode 100644 index 0000000000..b8960dcdf4 --- /dev/null +++ b/internal/core/plugin_daemon/tool.gen.go @@ -0,0 +1,36 @@ +// Code generated by controller generator. DO NOT EDIT. + +package plugin_daemon + +import ( + "github.com/langgenius/dify-plugin-daemon/internal/core/session_manager" + "github.com/langgenius/dify-plugin-daemon/internal/utils/stream" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/requests" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/tool_entities" +) + +func ValidateToolCredentials( + session *session_manager.Session, + request *requests.RequestValidateToolCredentials, +) ( + *stream.Stream[tool_entities.ValidateCredentialsResult], error, +) { + return GenericInvokePlugin[requests.RequestValidateToolCredentials, tool_entities.ValidateCredentialsResult]( + session, + request, + 1, + ) +} + +func GetToolRuntimeParameters( + session *session_manager.Session, + request *requests.RequestGetToolRuntimeParameters, +) ( + *stream.Stream[tool_entities.GetToolRuntimeParametersResponse], error, +) { + return GenericInvokePlugin[requests.RequestGetToolRuntimeParameters, tool_entities.GetToolRuntimeParametersResponse]( + session, + request, + 1, + ) +} diff --git a/internal/core/plugin_daemon/tool_service.go b/internal/core/plugin_daemon/tool_service.go index 77f21bfb0e..b314b8e27b 100644 --- a/internal/core/plugin_daemon/tool_service.go +++ b/internal/core/plugin_daemon/tool_service.go @@ -118,29 +118,3 @@ func bindToolValidator( } }) } - -func ValidateToolCredentials( - session *session_manager.Session, - request *requests.RequestValidateToolCredentials, -) ( - *stream.Stream[tool_entities.ValidateCredentialsResult], error, -) { - return GenericInvokePlugin[requests.RequestValidateToolCredentials, tool_entities.ValidateCredentialsResult]( - session, - request, - 1, - ) -} - -func GetToolRuntimeParameters( - session *session_manager.Session, - request *requests.RequestGetToolRuntimeParameters, -) ( - *stream.Stream[tool_entities.GetToolRuntimeParametersResponse], error, -) { - return GenericInvokePlugin[requests.RequestGetToolRuntimeParameters, tool_entities.GetToolRuntimeParametersResponse]( - session, - request, - 1, - ) -} diff --git a/internal/core/plugin_manager/debugging_runtime/server_test.go b/internal/core/plugin_manager/debugging_runtime/server_test.go index 29d96e7fb5..49a2035817 100644 --- a/internal/core/plugin_manager/debugging_runtime/server_test.go +++ b/internal/core/plugin_manager/debugging_runtime/server_test.go @@ -9,9 +9,10 @@ import ( "time" "github.com/google/uuid" + cloudoss "github.com/langgenius/dify-cloud-kit/oss" + "github.com/langgenius/dify-cloud-kit/oss/factory" "github.com/langgenius/dify-plugin-daemon/internal/core/plugin_manager/media_transport" "github.com/langgenius/dify-plugin-daemon/internal/db" - "github.com/langgenius/dify-plugin-daemon/internal/oss/local" "github.com/langgenius/dify-plugin-daemon/internal/types/app" "github.com/langgenius/dify-plugin-daemon/internal/utils/cache" "github.com/langgenius/dify-plugin-daemon/internal/utils/network" @@ -58,8 +59,15 @@ func preparePluginServer(t *testing.T) (*RemotePluginServer, uint16) { t.Errorf("failed to get random port: %s", err.Error()) return nil, 0 } - - oss := local.NewLocalStorage("./storage") + oss, err := factory.Load("local", cloudoss.OSSArgs{ + Local: &cloudoss.Local{ + Path: "./storage", + }, + }, + ) + if err != nil { + t.Error("failed to load local storage", err.Error()) + } // start plugin server return NewRemotePluginServer(&app.Config{ diff --git a/internal/core/plugin_manager/install_to_local.go b/internal/core/plugin_manager/install_to_local.go index 509c610ab3..47bf99022c 100644 --- a/internal/core/plugin_manager/install_to_local.go +++ b/internal/core/plugin_manager/install_to_local.go @@ -3,6 +3,7 @@ package plugin_manager import ( "time" + "errors" "github.com/langgenius/dify-plugin-daemon/internal/utils/log" "github.com/langgenius/dify-plugin-daemon/internal/utils/routine" "github.com/langgenius/dify-plugin-daemon/internal/utils/stream" @@ -63,16 +64,24 @@ func (p *PluginManager) InstallToLocal( case err := <-errChan: if err != nil { // if error occurs, delete the plugin from local and stop the plugin - identity, err := runtime.Identity() - if err != nil { - log.Error("get plugin identity failed: %s", err.Error()) + identity, er := runtime.Identity() + if er != nil { + log.Error("get plugin identity failed: %s", er.Error()) } - if err := p.installedBucket.Delete(identity); err != nil { - log.Error("delete plugin from local failed: %s", err.Error()) + if er := p.installedBucket.Delete(identity); er != nil { + log.Error("delete plugin from local failed: %s", er.Error()) } + + var errorMsg string + if er != nil { + errorMsg = errors.Join(err, er).Error() + } else { + errorMsg = err.Error() + } + response.Write(PluginInstallResponse{ Event: PluginInstallEventError, - Data: err.Error(), + Data: errorMsg, }) runtime.Stop() return diff --git a/internal/core/plugin_manager/install_to_serverless.go b/internal/core/plugin_manager/install_to_serverless.go index 424d4c9651..32ab538fc6 100644 --- a/internal/core/plugin_manager/install_to_serverless.go +++ b/internal/core/plugin_manager/install_to_serverless.go @@ -35,7 +35,7 @@ func (p *PluginManager) InstallToAWSFromPkg( } // serverless.LaunchPlugin will check if the plugin has already been launched, if so, it returns directly - response, err := serverless.LaunchPlugin(originalPackager, decoder, p.serverlessConnectorLaunchTimeout, false) + response, err := serverless.LaunchPlugin(originalPackager, decoder, p.config.DifyPluginServerlessConnectorLaunchTimeout, false) if err != nil { return nil, err } @@ -158,7 +158,7 @@ func (p *PluginManager) ReinstallToAWSFromPkg( response, err := serverless.LaunchPlugin( originalPackager, decoder, - p.serverlessConnectorLaunchTimeout, + p.config.DifyPluginServerlessConnectorLaunchTimeout, true, // ignoreIdempotent, true means always reinstall ) if err != nil { diff --git a/internal/core/plugin_manager/launcher.go b/internal/core/plugin_manager/launcher.go index ceeddd6a04..32699768d4 100644 --- a/internal/core/plugin_manager/launcher.go +++ b/internal/core/plugin_manager/launcher.go @@ -48,7 +48,7 @@ func (p *PluginManager) getLocalPluginRuntime(pluginUniqueIdentifier plugin_enti identity := manifest.Identity() identity = strings.ReplaceAll(identity, ":", "-") - pluginWorkingPath := path.Join(p.workingDirectory, fmt.Sprintf("%s@%s", identity, checksum)) + pluginWorkingPath := path.Join(p.config.PluginWorkingPath, fmt.Sprintf("%s@%s", identity, checksum)) return &pluginRuntimeWithDecoder{ runtime: plugin_entities.PluginRuntime{ Config: manifest, @@ -130,18 +130,18 @@ func (p *PluginManager) launchLocal(pluginUniqueIdentifier plugin_entities.Plugi } localPluginRuntime := local_runtime.NewLocalPluginRuntime(local_runtime.LocalPluginRuntimeConfig{ - PythonInterpreterPath: p.pythonInterpreterPath, - UvPath: p.uvPath, - PythonEnvInitTimeout: p.pythonEnvInitTimeout, - PythonCompileAllExtraArgs: p.pythonCompileAllExtraArgs, - HttpProxy: p.HttpProxy, - HttpsProxy: p.HttpsProxy, - NoProxy: p.NoProxy, - PipMirrorUrl: p.pipMirrorUrl, - PipPreferBinary: p.pipPreferBinary, - PipExtraArgs: p.pipExtraArgs, - StdoutBufferSize: p.pluginStdioBufferSize, - StdoutMaxBufferSize: p.pluginStdioMaxBufferSize, + PythonInterpreterPath: p.config.PythonInterpreterPath, + UvPath: p.config.UvPath, + PythonEnvInitTimeout: p.config.PythonEnvInitTimeout, + PythonCompileAllExtraArgs: p.config.PythonCompileAllExtraArgs, + HttpProxy: p.config.HttpProxy, + HttpsProxy: p.config.HttpsProxy, + NoProxy: p.config.NoProxy, + PipMirrorUrl: p.config.PipMirrorUrl, + PipPreferBinary: *p.config.PipPreferBinary, + PipExtraArgs: p.config.PipExtraArgs, + StdoutBufferSize: p.config.PluginStdioBufferSize, + StdoutMaxBufferSize: p.config.PluginStdioMaxBufferSize, }) localPluginRuntime.PluginRuntime = plugin.runtime localPluginRuntime.BasicChecksum = basic_runtime.BasicChecksum{ diff --git a/internal/core/plugin_manager/lifecycle/full_duplex.go b/internal/core/plugin_manager/lifecycle/full_duplex.go index 9603316103..cfc7832647 100644 --- a/internal/core/plugin_manager/lifecycle/full_duplex.go +++ b/internal/core/plugin_manager/lifecycle/full_duplex.go @@ -97,8 +97,10 @@ func FullDuplex( <-c } - // restart plugin in 5s - time.Sleep(5 * time.Second) + // restart plugin in 5s (skip for debugging runtime) + if r.Type() != plugin_entities.PLUGIN_RUNTIME_TYPE_REMOTE { + time.Sleep(5 * time.Second) + } // add restart times r.AddRestarts() diff --git a/internal/core/plugin_manager/local_runtime/stdio.go b/internal/core/plugin_manager/local_runtime/stdio.go index bf82ba1ed1..3e9f13da73 100644 --- a/internal/core/plugin_manager/local_runtime/stdio.go +++ b/internal/core/plugin_manager/local_runtime/stdio.go @@ -156,19 +156,10 @@ func (s *stdioHolder) StartStdout(notify_heartbeat func()) { func(session_id string, data []byte) { // FIX: avoid deadlock to plugin invoke s.l.Lock() - tasks := []func(){} - for listener_session_id, listener := range s.listener { - // copy the listener to avoid reference issue - listener := listener - if listener_session_id == session_id { - tasks = append(tasks, func() { - listener(data) - }) - } - } + listener := s.listener[session_id] s.l.Unlock() - for _, t := range tasks { - t() + if listener != nil { + listener(data) } }, func() { diff --git a/internal/core/plugin_manager/manager.go b/internal/core/plugin_manager/manager.go index 19ace3ac03..696cbc76b5 100644 --- a/internal/core/plugin_manager/manager.go +++ b/internal/core/plugin_manager/manager.go @@ -4,14 +4,15 @@ import ( "errors" "fmt" "os" + "strings" + "github.com/langgenius/dify-cloud-kit/oss" "github.com/langgenius/dify-plugin-daemon/internal/core/dify_invocation" "github.com/langgenius/dify-plugin-daemon/internal/core/dify_invocation/real" "github.com/langgenius/dify-plugin-daemon/internal/core/plugin_manager/debugging_runtime" "github.com/langgenius/dify-plugin-daemon/internal/core/plugin_manager/media_transport" serverless "github.com/langgenius/dify-plugin-daemon/internal/core/plugin_manager/serverless_connector" "github.com/langgenius/dify-plugin-daemon/internal/db" - "github.com/langgenius/dify-plugin-daemon/internal/oss" "github.com/langgenius/dify-plugin-daemon/internal/types/app" "github.com/langgenius/dify-plugin-daemon/internal/types/models" "github.com/langgenius/dify-plugin-daemon/internal/utils/cache/helper" @@ -27,15 +28,6 @@ import ( type PluginManager struct { m mapping.Map[string, plugin_entities.PluginLifetime] - // max size of a plugin package - maxPluginPackageSize int64 - - // where the plugin finally running - workingDirectory string - - // where the plugin finally installed but not running - pluginStoragePath string - // mediaBucket is used to manage media files like plugin icons, images, etc. mediaBucket *media_transport.MediaBucket @@ -54,50 +46,13 @@ type PluginManager struct { // backwardsInvocation is a handle to invoke dify backwardsInvocation dify_invocation.BackwardsInvocation - // python interpreter path - pythonInterpreterPath string - - // uv path - uvPath string - - // python env init timeout - pythonEnvInitTimeout int - - // proxy settings - HttpProxy string - HttpsProxy string - NoProxy string - - // pip mirror url - pipMirrorUrl string - - // pip prefer binary - pipPreferBinary bool - - // pip verbose - pipVerbose bool - - // pip extra args - pipExtraArgs string - - // python compileall extra args - pythonCompileAllExtraArgs string + config *app.Config // remote plugin server remotePluginServer debugging_runtime.RemotePluginServerInterface // max launching lock to prevent too many plugins launching at the same time maxLaunchingLock chan bool - - // platform, local or serverless - platform app.PlatformType - - // serverless connector launch timeout - serverlessConnectorLaunchTimeout int - - // plugin stdio buffer size - pluginStdioBufferSize int - pluginStdioMaxBufferSize int } var ( @@ -106,9 +61,6 @@ var ( func InitGlobalManager(oss oss.OSS, configuration *app.Config) *PluginManager { manager = &PluginManager{ - maxPluginPackageSize: configuration.MaxPluginPackageSize, - pluginStoragePath: configuration.PluginInstalledPath, - workingDirectory: configuration.PluginWorkingPath, mediaBucket: media_transport.NewAssetsBucket( oss, configuration.PluginMediaCachePath, @@ -122,23 +74,10 @@ func InitGlobalManager(oss oss.OSS, configuration *app.Config) *PluginManager { oss, configuration.PluginInstalledPath, ), - localPluginLaunchingLock: lock.NewGranularityLock(), - maxLaunchingLock: make(chan bool, 2), // by default, we allow 2 plugins launching at the same time - pythonInterpreterPath: configuration.PythonInterpreterPath, - uvPath: configuration.UvPath, - pythonEnvInitTimeout: configuration.PythonEnvInitTimeout, - pythonCompileAllExtraArgs: configuration.PythonCompileAllExtraArgs, - platform: configuration.Platform, - HttpProxy: configuration.HttpProxy, - HttpsProxy: configuration.HttpsProxy, - NoProxy: configuration.NoProxy, - pipMirrorUrl: configuration.PipMirrorUrl, - pipPreferBinary: *configuration.PipPreferBinary, - pipVerbose: *configuration.PipVerbose, - pipExtraArgs: configuration.PipExtraArgs, - serverlessConnectorLaunchTimeout: configuration.DifyPluginServerlessConnectorLaunchTimeout, - pluginStdioBufferSize: configuration.PluginStdioBufferSize, - pluginStdioMaxBufferSize: configuration.PluginStdioMaxBufferSize, + localPluginLaunchingLock: lock.NewGranularityLock(), + // By default, we allow up to configuration.PluginLocalLaunchingConcurrent plugins to be launched concurrently; if not configured, the default is 2. + maxLaunchingLock: make(chan bool, configuration.PluginLocalLaunchingConcurrent), + config: configuration, } return manager @@ -151,7 +90,7 @@ func Manager() *PluginManager { func (p *PluginManager) Get( identity plugin_entities.PluginUniqueIdentifier, ) (plugin_entities.PluginLifetime, error) { - if identity.RemoteLike() || p.platform == app.PLATFORM_LOCAL { + if identity.RemoteLike() || p.config.Platform == app.PLATFORM_LOCAL { // check if it's a debugging plugin or a local plugin if v, ok := p.m.Load(identity.String()); ok { return v, nil @@ -175,6 +114,7 @@ func (p *PluginManager) GetAsset(id string) ([]byte, error) { func (p *PluginManager) Launch(configuration *app.Config) { log.Info("start plugin manager daemon...") + // init redis client if configuration.DBType == "mysql" && configuration.CacheScheme == "mysql" { err := db.DifyPluginDB.AutoMigrate( mysql.CacheKV{}, @@ -186,8 +126,24 @@ func (p *PluginManager) Launch(configuration *app.Config) { log.Panic("init mysql cache tables failed: %s", err.Error()) } mysql.InitMysqlClient() + } else + if configuration.RedisUseSentinel { + // use Redis Sentinel + sentinels := strings.Split(configuration.RedisSentinels, ",") + if err := redis.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 { - // init redis client if err := redis.InitRedisClient( fmt.Sprintf("%s:%d", configuration.RedisHost, configuration.RedisPort), configuration.RedisUser, @@ -214,7 +170,7 @@ func (p *PluginManager) Launch(configuration *app.Config) { // start local watcher if configuration.Platform == app.PLATFORM_LOCAL { - p.startLocalWatcher() + p.startLocalWatcher(configuration) } // launch serverless connector diff --git a/internal/core/plugin_manager/media_transport/assets.go b/internal/core/plugin_manager/media_transport/assets.go index db72c87c3e..bf98e83871 100644 --- a/internal/core/plugin_manager/media_transport/assets.go +++ b/internal/core/plugin_manager/media_transport/assets.go @@ -34,62 +34,38 @@ func (m *MediaBucket) RemapAssets(declaration *plugin_entities.PluginDeclaration var err error if declaration.Model != nil { - if declaration.Model.IconSmall != nil { - if declaration.Model.IconSmall.EnUS != "" { - declaration.Model.IconSmall.EnUS, err = remap(declaration.Model.IconSmall.EnUS) - if err != nil { - return nil, errors.Join(err, fmt.Errorf("failed to remap model icon small en_US")) - } - } - - if declaration.Model.IconSmall.ZhHans != "" { - declaration.Model.IconSmall.ZhHans, err = remap(declaration.Model.IconSmall.ZhHans) - if err != nil { - return nil, errors.Join(err, fmt.Errorf("failed to remap model icon small zh_Hans")) - } - } - - if declaration.Model.IconSmall.JaJp != "" { - declaration.Model.IconSmall.JaJp, err = remap(declaration.Model.IconSmall.JaJp) - if err != nil { - return nil, errors.Join(err, fmt.Errorf("failed to remap model icon small ja_JP")) - } - } - - if declaration.Model.IconSmall.PtBr != "" { - declaration.Model.IconSmall.PtBr, err = remap(declaration.Model.IconSmall.PtBr) - if err != nil { - return nil, errors.Join(err, fmt.Errorf("failed to remap model icon small pt_BR")) - } - } + iconFields := []struct { + icon *plugin_entities.I18nObject + iconType string + fieldName string + }{ + {declaration.Model.IconSmall, "model icon small", ""}, + {declaration.Model.IconLarge, "model icon large", ""}, + {declaration.Model.IconSmallDark, "model icon small dark", ""}, + {declaration.Model.IconLargeDark, "model icon large dark", ""}, } - if declaration.Model.IconLarge != nil { - if declaration.Model.IconLarge.EnUS != "" { - declaration.Model.IconLarge.EnUS, err = remap(declaration.Model.IconLarge.EnUS) - if err != nil { - return nil, errors.Join(err, fmt.Errorf("failed to remap model icon large en_US")) - } - } - - if declaration.Model.IconLarge.ZhHans != "" { - declaration.Model.IconLarge.ZhHans, err = remap(declaration.Model.IconLarge.ZhHans) - if err != nil { - return nil, errors.Join(err, fmt.Errorf("failed to remap model icon large zh_Hans")) - } - } + langFields := []struct { + get func(*plugin_entities.I18nObject) *string + suffix string + }{ + {func(i *plugin_entities.I18nObject) *string { return &i.EnUS }, "en_US"}, + {func(i *plugin_entities.I18nObject) *string { return &i.ZhHans }, "zh_Hans"}, + {func(i *plugin_entities.I18nObject) *string { return &i.JaJp }, "ja_JP"}, + {func(i *plugin_entities.I18nObject) *string { return &i.PtBr }, "pt_BR"}, + } - if declaration.Model.IconLarge.JaJp != "" { - declaration.Model.IconLarge.JaJp, err = remap(declaration.Model.IconLarge.JaJp) - if err != nil { - return nil, errors.Join(err, fmt.Errorf("failed to remap model icon large ja_JP")) - } + for _, iconField := range iconFields { + if iconField.icon == nil { + continue } - - if declaration.Model.IconLarge.PtBr != "" { - declaration.Model.IconLarge.PtBr, err = remap(declaration.Model.IconLarge.PtBr) - if err != nil { - return nil, errors.Join(err, fmt.Errorf("failed to remap model icon large pt_BR")) + for _, langField := range langFields { + valPtr := langField.get(iconField.icon) + if valPtr != nil && *valPtr != "" { + *valPtr, err = remap(*valPtr) + if err != nil { + return nil, errors.Join(err, fmt.Errorf("failed to remap %s %s", iconField.iconType, langField.suffix)) + } } } } @@ -102,6 +78,13 @@ func (m *MediaBucket) RemapAssets(declaration *plugin_entities.PluginDeclaration return nil, errors.Join(err, fmt.Errorf("failed to remap tool icon")) } } + + if declaration.Tool.Identity.IconDark != "" { + declaration.Tool.Identity.IconDark, err = remap(declaration.Tool.Identity.IconDark) + if err != nil { + return nil, errors.Join(err, fmt.Errorf("failed to remap tool icon dark")) + } + } } if declaration.AgentStrategy != nil { @@ -111,6 +94,13 @@ func (m *MediaBucket) RemapAssets(declaration *plugin_entities.PluginDeclaration return nil, errors.Join(err, fmt.Errorf("failed to remap agent icon")) } } + + if declaration.AgentStrategy.Identity.IconDark != "" { + declaration.AgentStrategy.Identity.IconDark, err = remap(declaration.AgentStrategy.Identity.IconDark) + if err != nil { + return nil, errors.Join(err, fmt.Errorf("failed to remap agent icon dark")) + } + } } if declaration.Icon != "" { @@ -120,5 +110,12 @@ func (m *MediaBucket) RemapAssets(declaration *plugin_entities.PluginDeclaration } } + if declaration.IconDark != "" { + declaration.IconDark, err = remap(declaration.IconDark) + if err != nil { + return nil, errors.Join(err, fmt.Errorf("failed to remap plugin dark icon")) + } + } + return assetsIds, nil } diff --git a/internal/core/plugin_manager/media_transport/assets_bucket.go b/internal/core/plugin_manager/media_transport/assets_bucket.go index 36c35bbbd9..97f132559b 100644 --- a/internal/core/plugin_manager/media_transport/assets_bucket.go +++ b/internal/core/plugin_manager/media_transport/assets_bucket.go @@ -7,7 +7,7 @@ import ( "path/filepath" lru "github.com/hashicorp/golang-lru/v2" - "github.com/langgenius/dify-plugin-daemon/internal/oss" + "github.com/langgenius/dify-cloud-kit/oss" ) type MediaBucket struct { diff --git a/internal/core/plugin_manager/media_transport/installed_bucket.go b/internal/core/plugin_manager/media_transport/installed_bucket.go index 3ccbdd1103..bde53d0974 100644 --- a/internal/core/plugin_manager/media_transport/installed_bucket.go +++ b/internal/core/plugin_manager/media_transport/installed_bucket.go @@ -5,7 +5,8 @@ import ( "regexp" "strings" - "github.com/langgenius/dify-plugin-daemon/internal/oss" + "github.com/langgenius/dify-cloud-kit/oss" + "github.com/langgenius/dify-plugin-daemon/internal/utils/log" "github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities" ) @@ -77,7 +78,8 @@ func (b *InstalledBucket) List() ([]plugin_entities.PluginUniqueIdentifier, erro strings.TrimPrefix(path.Path, b.installedPath), ) if err != nil { - return nil, err + log.Error("failed to create PluginUniqueIdentifier from path %s: %v", path.Path, err) + continue } identifiers = append(identifiers, identifier) } diff --git a/internal/core/plugin_manager/media_transport/package_bucket.go b/internal/core/plugin_manager/media_transport/package_bucket.go index b59609108e..3cd3cba8ad 100644 --- a/internal/core/plugin_manager/media_transport/package_bucket.go +++ b/internal/core/plugin_manager/media_transport/package_bucket.go @@ -3,7 +3,7 @@ package media_transport import ( "path" - "github.com/langgenius/dify-plugin-daemon/internal/oss" + "github.com/langgenius/dify-cloud-kit/oss" ) type PackageBucket struct { diff --git a/internal/core/plugin_manager/serverless.go b/internal/core/plugin_manager/serverless.go index ddd6d10045..f52222fc33 100644 --- a/internal/core/plugin_manager/serverless.go +++ b/internal/core/plugin_manager/serverless.go @@ -44,14 +44,15 @@ func (p *PluginManager) getServerlessPluginRuntime( runtimeEntity.InitState() // convert to plugin runtime - pluginRuntime := serverless_runtime.AWSPluginRuntime{ + pluginRuntime := serverless_runtime.ServerlessPluginRuntime{ BasicChecksum: basic_runtime.BasicChecksum{ MediaTransport: basic_runtime.NewMediaTransport(p.mediaBucket), InnerChecksum: model.Checksum, }, - PluginRuntime: runtimeEntity, - LambdaURL: model.FunctionURL, - LambdaName: model.FunctionName, + PluginRuntime: runtimeEntity, + LambdaURL: model.FunctionURL, + LambdaName: model.FunctionName, + PluginMaxExecutionTimeout: p.config.PluginMaxExecutionTimeout, } if err := pluginRuntime.InitEnvironment(); err != nil { diff --git a/internal/core/plugin_manager/serverless_runtime/environment.go b/internal/core/plugin_manager/serverless_runtime/environment.go index e0b03974d4..14c10069af 100644 --- a/internal/core/plugin_manager/serverless_runtime/environment.go +++ b/internal/core/plugin_manager/serverless_runtime/environment.go @@ -1,6 +1,7 @@ package serverless_runtime import ( + "context" "fmt" "net" "net/http" @@ -9,22 +10,29 @@ import ( "github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities" ) -func (r *AWSPluginRuntime) InitEnvironment() error { +func (r *ServerlessPluginRuntime) InitEnvironment() error { // init http client r.client = &http.Client{ Transport: &http.Transport{ - Dial: (&net.Dialer{ - Timeout: 5 * time.Second, - KeepAlive: 120 * time.Second, - }).Dial, - IdleConnTimeout: 120 * time.Second, + TLSHandshakeTimeout: time.Duration(r.PluginMaxExecutionTimeout) * time.Second, + IdleConnTimeout: 120 * time.Second, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + conn, err := (&net.Dialer{ + Timeout: time.Duration(r.PluginMaxExecutionTimeout) * time.Second, + KeepAlive: 120 * time.Second, + }).DialContext(ctx, network, addr) + if err != nil { + return nil, err + } + return conn, nil + }, }, } return nil } -func (r *AWSPluginRuntime) Identity() (plugin_entities.PluginUniqueIdentifier, error) { +func (r *ServerlessPluginRuntime) Identity() (plugin_entities.PluginUniqueIdentifier, error) { checksum, err := r.Checksum() if err != nil { return "", err diff --git a/internal/core/plugin_manager/serverless_runtime/io.go b/internal/core/plugin_manager/serverless_runtime/io.go index bc39e758b1..91507f4336 100644 --- a/internal/core/plugin_manager/serverless_runtime/io.go +++ b/internal/core/plugin_manager/serverless_runtime/io.go @@ -3,13 +3,12 @@ package serverless_runtime import ( "bufio" "bytes" - "context" "fmt" - "net/http" + "io" "net/url" - "time" "github.com/langgenius/dify-plugin-daemon/internal/core/plugin_daemon/access_types" + "github.com/langgenius/dify-plugin-daemon/internal/utils/http_requests" "github.com/langgenius/dify-plugin-daemon/internal/utils/log" "github.com/langgenius/dify-plugin-daemon/internal/utils/parser" "github.com/langgenius/dify-plugin-daemon/internal/utils/routine" @@ -17,7 +16,7 @@ import ( "github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities" ) -func (r *AWSPluginRuntime) Listen(sessionId string) *entities.Broadcast[plugin_entities.SessionMessage] { +func (r *ServerlessPluginRuntime) Listen(sessionId string) *entities.Broadcast[plugin_entities.SessionMessage] { l := entities.NewBroadcast[plugin_entities.SessionMessage]() // store the listener r.listeners.Store(sessionId, l) @@ -25,7 +24,7 @@ func (r *AWSPluginRuntime) Listen(sessionId string) *entities.Broadcast[plugin_e } // For AWS Lambda, write is equivalent to http request, it's not a normal stream like stdio and tcp -func (r *AWSPluginRuntime) Write(sessionId string, action access_types.PluginAccessAction, data []byte) { +func (r *ServerlessPluginRuntime) Write(sessionId string, action access_types.PluginAccessAction, data []byte) { l, ok := r.listeners.Load(sessionId) if !ok { log.Error("session %s not found", sessionId) @@ -46,22 +45,6 @@ func (r *AWSPluginRuntime) Write(sessionId string, action access_types.PluginAcc return } - url += "?action=" + string(action) - - connectTime := 240 * time.Second - - // create a new http request - ctx, cancel := context.WithTimeout(context.Background(), connectTime) - time.AfterFunc(connectTime, cancel) - req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(data)) - if err != nil { - r.Error(fmt.Sprintf("Error creating request: %v", err)) - return - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "text/event-stream") - req.Header.Set("Dify-Plugin-Session-ID", sessionId) - routine.Submit(map[string]string{ "module": "serverless_runtime", "function": "Write", @@ -76,7 +59,18 @@ func (r *AWSPluginRuntime) Write(sessionId string, action access_types.PluginAcc Data: []byte(""), }) - response, err := r.client.Do(req) + // create a new http request to serverless runtimes + url += "?action=" + string(action) + response, err := http_requests.Request( + r.client, url, "POST", + http_requests.HttpHeader(map[string]string{ + "Content-Type": "application/json", + "Accept": "text/event-stream", + "Dify-Plugin-Session-ID": sessionId, + }), + http_requests.HttpPayloadReader(io.NopCloser(bytes.NewReader(data))), + http_requests.HttpReadTimeout(int64(r.PluginMaxExecutionTimeout*1000)), + ) if err != nil { l.Send(plugin_entities.SessionMessage{ Type: plugin_entities.SESSION_MESSAGE_TYPE_ERROR, diff --git a/internal/core/plugin_manager/serverless_runtime/run.go b/internal/core/plugin_manager/serverless_runtime/run.go index 027bf884d6..afeafc4365 100644 --- a/internal/core/plugin_manager/serverless_runtime/run.go +++ b/internal/core/plugin_manager/serverless_runtime/run.go @@ -2,14 +2,14 @@ package serverless_runtime import "github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities" -func (r *AWSPluginRuntime) StartPlugin() error { +func (r *ServerlessPluginRuntime) StartPlugin() error { return nil } -func (r *AWSPluginRuntime) Wait() (<-chan bool, error) { +func (r *ServerlessPluginRuntime) Wait() (<-chan bool, error) { return nil, nil } -func (r *AWSPluginRuntime) Type() plugin_entities.PluginRuntimeType { +func (r *ServerlessPluginRuntime) Type() plugin_entities.PluginRuntimeType { return plugin_entities.PLUGIN_RUNTIME_TYPE_SERVERLESS } diff --git a/internal/core/plugin_manager/serverless_runtime/type.go b/internal/core/plugin_manager/serverless_runtime/type.go index 90c5c1b869..4f08eedd8f 100644 --- a/internal/core/plugin_manager/serverless_runtime/type.go +++ b/internal/core/plugin_manager/serverless_runtime/type.go @@ -9,7 +9,7 @@ import ( "github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities" ) -type AWSPluginRuntime struct { +type ServerlessPluginRuntime struct { basic_runtime.BasicChecksum plugin_entities.PluginRuntime @@ -21,4 +21,6 @@ type AWSPluginRuntime struct { listeners mapping.Map[string, *entities.Broadcast[plugin_entities.SessionMessage]] client *http.Client + + PluginMaxExecutionTimeout int // in seconds } diff --git a/internal/core/plugin_manager/watcher.go b/internal/core/plugin_manager/watcher.go index 5e1900c2ea..75340f4b39 100644 --- a/internal/core/plugin_manager/watcher.go +++ b/internal/core/plugin_manager/watcher.go @@ -1,6 +1,7 @@ package plugin_manager import ( + "sync" "time" "github.com/langgenius/dify-plugin-daemon/internal/core/plugin_manager/debugging_runtime" @@ -11,12 +12,13 @@ import ( "github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities" ) -func (p *PluginManager) startLocalWatcher() { +func (p *PluginManager) startLocalWatcher(config *app.Config) { go func() { - log.Info("start to handle new plugins in path: %s", p.pluginStoragePath) - p.handleNewLocalPlugins() + log.Info("start to handle new plugins in path: %s", p.config.PluginInstalledPath) + log.Info("Launching plugins with max concurrency: %d", p.config.PluginLocalLaunchingConcurrent) + p.handleNewLocalPlugins(config) for range time.NewTicker(time.Second * 30).C { - p.handleNewLocalPlugins() + p.handleNewLocalPlugins(config) p.removeUninstalledLocalPlugins() } }() @@ -66,7 +68,7 @@ func (p *PluginManager) startRemoteWatcher(config *app.Config) { } } -func (p *PluginManager) handleNewLocalPlugins() { +func (p *PluginManager) handleNewLocalPlugins(config *app.Config) { // walk through all plugins plugins, err := p.installedBucket.List() if err != nil { @@ -74,20 +76,50 @@ func (p *PluginManager) handleNewLocalPlugins() { return } + var wg sync.WaitGroup + maxConcurrency := config.PluginLocalLaunchingConcurrent + sem := make(chan struct{}, maxConcurrency) + for _, plugin := range plugins { - _, launchedChan, errChan, err := p.launchLocal(plugin) - if err != nil { - log.Error("launch local plugin failed: %s", err.Error()) - } + wg.Add(1) + // Fix closure issue: create local variable copy + currentPlugin := plugin + routine.Submit(map[string]string{ + "module": "plugin_manager", + "function": "handleNewLocalPlugins", + }, func() { + // Acquire sem inside goroutine + sem <- struct{}{} + defer func() { + if err := recover(); err != nil { + log.Error("plugin launch runtime error: %v", err) + } + <-sem + wg.Done() + }() - // consume error, avoid deadlock - for err := range errChan { - log.Error("plugin launch error: %s", err.Error()) - } + _, launchedChan, errChan, err := p.launchLocal(currentPlugin) + if err != nil { + log.Error("launch local plugin failed: %s", err.Error()) + return + } + + // Handle error channel + if errChan != nil { + for err := range errChan { + log.Error("plugin launch error: %s", err.Error()) + } + } - // wait for plugin launched - <-launchedChan + // Wait for plugin to complete startup + if launchedChan != nil { + <-launchedChan + } + }) } + + // wait for all plugins to be launched + wg.Wait() } // an async function to remove uninstalled local plugins diff --git a/internal/core/plugin_manager/watcher_test.go b/internal/core/plugin_manager/watcher_test.go index 8b098252db..8efcd2391f 100644 --- a/internal/core/plugin_manager/watcher_test.go +++ b/internal/core/plugin_manager/watcher_test.go @@ -5,9 +5,11 @@ import ( "time" "github.com/google/uuid" + cloudoss "github.com/langgenius/dify-cloud-kit/oss" + + "github.com/langgenius/dify-cloud-kit/oss/factory" "github.com/langgenius/dify-plugin-daemon/internal/core/plugin_daemon/access_types" "github.com/langgenius/dify-plugin-daemon/internal/core/plugin_manager/basic_runtime" - "github.com/langgenius/dify-plugin-daemon/internal/oss/local" "github.com/langgenius/dify-plugin-daemon/internal/types/app" "github.com/langgenius/dify-plugin-daemon/internal/utils/routine" "github.com/langgenius/dify-plugin-daemon/pkg/entities" @@ -111,7 +113,16 @@ func TestRemotePluginWatcherPluginStoredToManager(t *testing.T) { config := &app.Config{} config.SetDefault() routine.InitPool(1024) - oss := local.NewLocalStorage("./storage") + oss, err := factory.Load("local", cloudoss.OSSArgs{ + Local: &cloudoss.Local{ + Path: "./storage", + }, + }, + ) + if err != nil { + t.Error("failed to load local storage", err.Error()) + } + pm := InitGlobalManager(oss, config) pm.remotePluginServer = &fakeRemotePluginServer{} pm.startRemoteWatcher(config) diff --git a/internal/core/session_manager/session.go b/internal/core/session_manager/session.go index 42895632d0..01a7ac761c 100644 --- a/internal/core/session_manager/session.go +++ b/internal/core/session_manager/session.go @@ -35,10 +35,11 @@ type Session struct { Declaration *plugin_entities.PluginDeclaration `json:"declaration"` // information about incoming request - ConversationID *string `json:"conversation_id"` - MessageID *string `json:"message_id"` - AppID *string `json:"app_id"` - EndpointID *string `json:"endpoint_id"` + ConversationID *string `json:"conversation_id"` + MessageID *string `json:"message_id"` + AppID *string `json:"app_id"` + EndpointID *string `json:"endpoint_id"` + Context map[string]any `json:"context"` } func sessionKey(id string) string { @@ -59,6 +60,7 @@ type NewSessionPayload struct { MessageID *string `json:"message_id"` AppID *string `json:"app_id"` EndpointID *string `json:"endpoint_id"` + Context map[string]any `json:"context"` } func NewSession(payload NewSessionPayload) *Session { @@ -76,6 +78,7 @@ func NewSession(payload NewSessionPayload) *Session { MessageID: payload.MessageID, AppID: payload.AppID, EndpointID: payload.EndpointID, + Context: payload.Context, } session_lock.Lock() @@ -172,6 +175,7 @@ func (s *Session) Message(event PLUGIN_IN_STREAM_EVENT, data any) []byte { "message_id": s.MessageID, "app_id": s.AppID, "endpoint_id": s.EndpointID, + "context": s.Context, "event": event, "data": data, }) diff --git a/internal/db/init.go b/internal/db/init.go index 7138054b4f..610292cbc6 100644 --- a/internal/db/init.go +++ b/internal/db/init.go @@ -58,31 +58,35 @@ func autoMigrate() error { func Init(config *app.Config) { var err error if config.DBType == "postgresql" { - DifyPluginDB, err = pg.InitPluginDB( - config.DBHost, - int(config.DBPort), - config.DBDatabase, - config.DBDefaultDatabase, - config.DBUsername, - config.DBPassword, - config.DBSslMode, - config.DBMaxIdleConns, - config.DBMaxOpenConns, - config.DBConnMaxLifetime, - ) + DifyPluginDB, err = pg.InitPluginDB(&pg.PGConfig{ + Host: config.DBHost, + Port: int(config.DBPort), + DBName: config.DBDatabase, + DefaultDBName: config.DBDefaultDatabase, + User: config.DBUsername, + Pass: config.DBPassword, + SSLMode: config.DBSslMode, + MaxIdleConns: config.DBMaxIdleConns, + MaxOpenConns: config.DBMaxOpenConns, + ConnMaxLifetime: config.DBConnMaxLifetime, + Charset: config.DBCharset, + Extras: config.DBExtras, + }) } else if config.DBType == "mysql" { - DifyPluginDB, err = mysql.InitPluginDB( - config.DBHost, - int(config.DBPort), - config.DBDatabase, - config.DBDefaultDatabase, - config.DBUsername, - config.DBPassword, - config.DBSslMode, - config.DBMaxIdleConns, - config.DBMaxOpenConns, - config.DBConnMaxLifetime, - ) + DifyPluginDB, err = mysql.InitPluginDB(&mysql.MySQLConfig{ + Host: config.DBHost, + Port: int(config.DBPort), + DBName: config.DBDatabase, + DefaultDBName: config.DBDefaultDatabase, + User: config.DBUsername, + Pass: config.DBPassword, + SSLMode: config.DBSslMode, + MaxIdleConns: config.DBMaxIdleConns, + MaxOpenConns: config.DBMaxOpenConns, + ConnMaxLifetime: config.DBConnMaxLifetime, + Charset: config.DBCharset, + Extras: config.DBExtras, + }) } else { log.Panic("unsupported database type: %v", config.DBType) } diff --git a/internal/db/mysql/mysql.go b/internal/db/mysql/mysql.go index c53df23a4e..6c679d4974 100644 --- a/internal/db/mysql/mysql.go +++ b/internal/db/mysql/mysql.go @@ -8,31 +8,47 @@ import ( "gorm.io/gorm" ) -func InitPluginDB(host string, port int, dbName string, defaultDbName string, user string, password string, sslMode string, maxIdleConns int, maxOpenConns int, connMaxLifetime int) (*gorm.DB, error) { +type MySQLConfig struct { + Host string + Port int + DBName string + DefaultDBName string + User string + Pass string + SSLMode string + MaxIdleConns int + MaxOpenConns int + ConnMaxLifetime int + Charset string + Extras string +} + +func InitPluginDB(config *MySQLConfig) (*gorm.DB, error) { + // TODO: MySQL dose not support DB_EXTRAS now initializer := mysqlDbInitializer{ - host: host, - port: port, - user: user, - password: password, - sslMode: sslMode, + host: config.Host, + port: config.Port, + user: config.User, + password: config.Pass, + sslMode: config.SSLMode, } // first try to connect to target database - db, err := initializer.connect(dbName) + db, err := initializer.connect(config.DBName) if err != nil { // if connection fails, try to create database - db, err = initializer.connect(defaultDbName) + db, err = initializer.connect(config.DefaultDBName) if err != nil { return nil, err } - err = initializer.createDatabaseIfNotExists(db, dbName) + err = initializer.createDatabaseIfNotExists(db, config.DBName) if err != nil { return nil, err } // connect to the new db - db, err = initializer.connect(dbName) + db, err = initializer.connect(config.DBName) if err != nil { return nil, err } @@ -44,9 +60,9 @@ func InitPluginDB(host string, port int, dbName string, defaultDbName string, us } // configure connection pool - pool.SetMaxIdleConns(maxIdleConns) - pool.SetMaxOpenConns(maxOpenConns) - pool.SetConnMaxLifetime(time.Duration(connMaxLifetime) * time.Second) + pool.SetMaxIdleConns(config.MaxIdleConns) + pool.SetMaxOpenConns(config.MaxOpenConns) + pool.SetConnMaxLifetime(time.Duration(config.ConnMaxLifetime) * time.Second) return db, nil } diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go index 3fd5c80a63..5f7d0b208d 100644 --- a/internal/db/pg/pg.go +++ b/internal/db/pg/pg.go @@ -2,19 +2,35 @@ package pg import ( "fmt" + "strings" "time" "gorm.io/driver/postgres" "gorm.io/gorm" ) -func InitPluginDB(host string, port int, db_name string, default_db_name string, user string, pass string, sslmode string, maxIdleConns int, maxOpenConns int, connMaxLifetime int) (*gorm.DB, error) { +type PGConfig struct { + Host string + Port int + DBName string + DefaultDBName string + User string + Pass string + SSLMode string + MaxIdleConns int + MaxOpenConns int + ConnMaxLifetime int + Charset string + Extras string +} + +func InitPluginDB(config *PGConfig) (*gorm.DB, error) { // first try to connect to target database - dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", host, port, user, pass, db_name, sslmode) + dsn := buildDSN(config, false) db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { // if connection fails, try to create database - dsn = fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", host, port, user, pass, default_db_name, sslmode) + dsn = buildDSN(config, true) db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { return nil, err @@ -27,21 +43,21 @@ func InitPluginDB(host string, port int, db_name string, default_db_name string, defer pgsqlDB.Close() // check if the db exists - rows, err := pgsqlDB.Query(fmt.Sprintf("SELECT 1 FROM pg_database WHERE datname = '%s'", db_name)) + rows, err := pgsqlDB.Query(fmt.Sprintf("SELECT 1 FROM pg_database WHERE datname = '%s'", config.DBName)) if err != nil { return nil, err } if !rows.Next() { // create database - _, err = pgsqlDB.Exec(fmt.Sprintf("CREATE DATABASE %s", db_name)) + _, err = pgsqlDB.Exec(fmt.Sprintf("CREATE DATABASE %s", config.DBName)) if err != nil { return nil, err } } // connect to the new db - dsn = fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", host, port, user, pass, db_name, sslmode) + dsn = fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", config.Host, config.Port, config.User, config.Pass, config.DBName, config.SSLMode) db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { return nil, err @@ -68,9 +84,33 @@ func InitPluginDB(host string, port int, db_name string, default_db_name string, } // configure connection pool - pgsqlDB.SetMaxIdleConns(maxIdleConns) - pgsqlDB.SetMaxOpenConns(maxOpenConns) - pgsqlDB.SetConnMaxLifetime(time.Duration(connMaxLifetime) * time.Second) - + pgsqlDB.SetMaxIdleConns(config.MaxIdleConns) + pgsqlDB.SetMaxOpenConns(config.MaxOpenConns) + pgsqlDB.SetConnMaxLifetime(time.Duration(config.ConnMaxLifetime) * time.Second) + return db, nil } + +func buildDSN(config *PGConfig, useDefaultDB bool) string { + dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", + config.Host, + config.Port, + config.User, + config.Pass, + func() string { + if useDefaultDB { + return config.DefaultDBName + } + return config.DBName + }(), + config.SSLMode, + ) + if config.Charset != "" { + dsn = fmt.Sprintf("%s client_encoding=%s", dsn, config.Charset) + } + if config.Extras != "" { + extra := strings.ReplaceAll(config.Extras, "options=", "") + dsn = fmt.Sprintf("%s options='%s'", dsn, extra) + } + return dsn +} diff --git a/internal/oss/aliyun/aliyun_oss_storage.go b/internal/oss/aliyun/aliyun_oss_storage.go deleted file mode 100644 index 66d144e416..0000000000 --- a/internal/oss/aliyun/aliyun_oss_storage.go +++ /dev/null @@ -1,215 +0,0 @@ -package aliyun - -import ( - "bytes" - "fmt" - "io" - "path" - "strings" - "time" - - "github.com/aliyun/aliyun-oss-go-sdk/oss" - dify_oss "github.com/langgenius/dify-plugin-daemon/internal/oss" -) - -type AliyunOSSStorage struct { - client *oss.Client - bucket *oss.Bucket - path string -} - -func NewAliyunOSSStorage( - region string, - endpoint string, - accessKeyID string, - accessKeySecret string, - authVersion string, - path string, - bucketName string, -) (*AliyunOSSStorage, error) { - // options - var options []oss.ClientOption - - // set region (required for v4) - if region != "" { - options = append(options, oss.Region(region)) - } - - // set auth-version - if authVersion == "v1" { - options = append(options, oss.AuthVersion(oss.AuthV1)) - } else if authVersion == "v4" { - options = append(options, oss.AuthVersion(oss.AuthV4)) - } else { - // default use v4 - options = append(options, oss.AuthVersion(oss.AuthV4)) - } - - // create client - var client *oss.Client - var err error - - client, err = oss.New(endpoint, accessKeyID, accessKeySecret, options...) - - if err != nil { - return nil, fmt.Errorf("failed to create AliyunOSS client: %w", err) - } - - // get specified bucket - bucket, err := client.Bucket(bucketName) - if err != nil { - return nil, fmt.Errorf("failed to get bucket %s: %w", bucketName, err) - } - - // normalize path: remove leading slash, ensure trailing slash - path = strings.TrimPrefix(path, "/") - if path != "" && !strings.HasSuffix(path, "/") { - path = path + "/" - } - - return &AliyunOSSStorage{ - client: client, - bucket: bucket, - path: path, - }, nil -} - -// combine full object path -func (s *AliyunOSSStorage) fullPath(key string) string { - return path.Join(s.path, key) -} - -func (s *AliyunOSSStorage) Save(key string, data []byte) error { - fullPath := s.fullPath(key) - err := s.bucket.PutObject(fullPath, bytes.NewReader(data)) - if err != nil { - return fmt.Errorf("failed to put object to Aliyun OSS: %w", err) - } - return nil -} - -func (s *AliyunOSSStorage) Load(key string) ([]byte, error) { - fullPath := s.fullPath(key) - object, err := s.bucket.GetObject(fullPath) - if err != nil { - return nil, fmt.Errorf("failed to get object from Aliyun OSS: %w", err) - } - defer object.Close() - - data, err := io.ReadAll(object) - if err != nil { - return nil, fmt.Errorf("failed to read object data from Aliyun OSS: %w", err) - } - return data, nil -} - -func (s *AliyunOSSStorage) Exists(key string) (bool, error) { - fullPath := s.fullPath(key) - exist, err := s.bucket.IsObjectExist(fullPath) - if err != nil { - return false, fmt.Errorf("failed to check if object exists in Aliyun OSS: %w", err) - } - return exist, nil -} - -func (s *AliyunOSSStorage) State(key string) (dify_oss.OSSState, error) { - fullPath := s.fullPath(key) - meta, err := s.bucket.GetObjectMeta(fullPath) - if err != nil { - return dify_oss.OSSState{}, fmt.Errorf("failed to get object meta from Aliyun OSS: %w", err) - } - - // Get content length - size := int64(0) - contentLength := meta.Get("Content-Length") - if contentLength != "" { - fmt.Sscanf(contentLength, "%d", &size) - } - - // Get last modified time - lastModified := time.Time{} - lastModifiedStr := meta.Get("Last-Modified") - if lastModifiedStr != "" { - lastModified, _ = time.Parse(time.RFC1123, lastModifiedStr) - } - - return dify_oss.OSSState{ - Size: size, - LastModified: lastModified, - }, nil -} - -func (s *AliyunOSSStorage) List(prefix string) ([]dify_oss.OSSPath, error) { - // combine given prefix with path - fullPrefix := path.Join(s.path, prefix) - - // Ensure the prefix ends with a slash for directories - if !strings.HasSuffix(fullPrefix, "/") { - fullPrefix = fullPrefix + "/" - } - - var paths []dify_oss.OSSPath - marker := "" - for { - lsRes, err := s.bucket.ListObjects(oss.Marker(marker), oss.Prefix(fullPrefix), oss.Delimiter("/")) - if err != nil { - return nil, fmt.Errorf("failed to list objects in Aliyun OSS: %w", err) - } - - // Add files - for _, object := range lsRes.Objects { - if object.Key == fullPrefix { - continue - } - // remove path and prefix from full path, only keep relative path - key := strings.TrimPrefix(object.Key, fullPrefix) - // Skip empty keys - if key == "" { - continue - } - paths = append(paths, dify_oss.OSSPath{ - Path: key, - IsDir: false, - }) - } - - // Add directories - for _, commonPrefix := range lsRes.CommonPrefixes { - if commonPrefix == fullPrefix { - continue - } - // remove path and prefix from full path, only keep relative path - dirPath := strings.TrimPrefix(commonPrefix, fullPrefix) - dirPath = strings.TrimSuffix(dirPath, "/") - if dirPath == "" { - continue - } - paths = append(paths, dify_oss.OSSPath{ - Path: dirPath, - IsDir: true, - }) - } - - // Check if there are more results - if lsRes.IsTruncated { - marker = lsRes.NextMarker - } else { - break - } - } - - return paths, nil -} - -func (s *AliyunOSSStorage) Delete(key string) error { - fullPath := s.fullPath(key) - err := s.bucket.DeleteObject(fullPath) - if err != nil { - return fmt.Errorf("failed to delete object from Aliyun OSS: %w", err) - } - return nil -} - -func (s *AliyunOSSStorage) Type() string { - return dify_oss.OSS_TYPE_ALIYUN_OSS -} diff --git a/internal/oss/azure/blob_storage.go b/internal/oss/azure/blob_storage.go deleted file mode 100644 index 57a201f8e7..0000000000 --- a/internal/oss/azure/blob_storage.go +++ /dev/null @@ -1,122 +0,0 @@ -package azure - -import ( - "bytes" - "context" - "strings" - - "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob" - "github.com/langgenius/dify-plugin-daemon/internal/oss" -) - -type AzureBlobStorage struct { - client *azblob.Client - containerName string -} - -func NewAzureBlobStorage(containerName string, connectionString string) (oss.OSS, error) { - client, err := azblob.NewClientFromConnectionString(connectionString, nil) - if err != nil { - return nil, err - } - - return &AzureBlobStorage{ - client: client, - containerName: containerName, - }, nil -} - -func (a *AzureBlobStorage) Save(key string, data []byte) error { - _, err := a.client.UploadBuffer(context.TODO(), a.containerName, key, data, nil) - return err -} - -func (a *AzureBlobStorage) Load(key string) ([]byte, error) { - get, err := a.client.DownloadStream(context.TODO(), a.containerName, key, nil) - if err != nil { - return nil, err - } - - downloadedData := bytes.Buffer{} - retryReader := get.NewRetryReader(context.TODO(), &azblob.RetryReaderOptions{}) - _, err = downloadedData.ReadFrom(retryReader) - if err != nil { - return nil, err - } - - err = retryReader.Close() - if err != nil { - return nil, err - } - - return downloadedData.Bytes(), nil -} - -func (a *AzureBlobStorage) Exists(key string) (bool, error) { - blobClient := a.client.ServiceClient().NewContainerClient(a.containerName).NewBlobClient(key) - _, err := blobClient.GetProperties(context.TODO(), nil) - - if err != nil { - if strings.Contains(err.Error(), "404") { - return false, nil - } - return false, err - } - - return true, nil -} - -func (a *AzureBlobStorage) State(key string) (oss.OSSState, error) { - blobClient := a.client.ServiceClient().NewContainerClient(a.containerName).NewBlobClient(key) - props, err := blobClient.GetProperties(context.TODO(), nil) - - if err != nil { - return oss.OSSState{}, err - } - - return oss.OSSState{ - Size: *props.ContentLength, - LastModified: *props.LastModified, - }, nil -} - -func (a *AzureBlobStorage) List(prefix string) ([]oss.OSSPath, error) { - // append a slash to the prefix if it doesn't end with one - if !strings.HasSuffix(prefix, "/") { - prefix = prefix + "/" - } - - pager := a.client.NewListBlobsFlatPager(a.containerName, &azblob.ListBlobsFlatOptions{ - Prefix: &prefix, - }) - - paths := make([]oss.OSSPath, 0) - for pager.More() { - page, err := pager.NextPage(context.TODO()) - if err != nil { - return nil, err - } - - for _, blob := range page.Segment.BlobItems { - // remove prefix - key := strings.TrimPrefix(*blob.Name, prefix) - // remove leading slash - key = strings.TrimPrefix(key, "/") - paths = append(paths, oss.OSSPath{ - Path: key, - IsDir: false, - }) - } - } - - return paths, nil -} - -func (a *AzureBlobStorage) Delete(key string) error { - _, err := a.client.DeleteBlob(context.TODO(), a.containerName, key, nil) - return err -} - -func (a *AzureBlobStorage) Type() string { - return oss.OSS_TYPE_AZURE_BLOB -} diff --git a/internal/oss/gcs/gcs_storage.go b/internal/oss/gcs/gcs_storage.go deleted file mode 100644 index 62314ac241..0000000000 --- a/internal/oss/gcs/gcs_storage.go +++ /dev/null @@ -1,151 +0,0 @@ -package gcs - -import ( - "context" - "errors" - "fmt" - "io" - "strings" - - "cloud.google.com/go/storage" - "github.com/langgenius/dify-plugin-daemon/internal/oss" - "github.com/langgenius/dify-plugin-daemon/internal/utils/log" - "google.golang.org/api/iterator" - "google.golang.org/api/option" -) - -type GCSStorage struct { - bucket *storage.BucketHandle -} - -func NewGCSStorage(ctx context.Context, bucketName string, opts ...option.ClientOption) (*GCSStorage, error) { - client, err := storage.NewClient(ctx, opts...) - if err != nil { - return nil, fmt.Errorf("create GCS client: %w", err) - } - - bucket := client.Bucket(bucketName) - // check if the bucket exists - _, err = bucket.Attrs(ctx) - if err != nil { - return nil, err - } - - return &GCSStorage{ - bucket: bucket, - }, nil -} - -func (s *GCSStorage) Type() string { - return oss.OSS_TYPE_GCS -} - -func (s *GCSStorage) Save(key string, data []byte) error { - ctx := context.TODO() - obj := s.bucket.Object(key) - w := obj.NewWriter(ctx) - defer func() { - if err := w.Close(); err != nil { - log.Error("failed to close GCS object writer: %v", err) - } - }() - - if _, err := w.Write(data); err != nil { - return fmt.Errorf("write data to GCS object %s/%s: %w", s.bucket.BucketName(), key, err) - } - return nil -} - -func (s *GCSStorage) Load(key string) ([]byte, error) { - ctx := context.TODO() - obj := s.bucket.Object(key) - - r, err := obj.NewReader(ctx) - if err != nil { - return nil, fmt.Errorf("create GCS object reader %s/%s: %w", s.bucket.BucketName(), key, err) - } - defer r.Close() - - data, err := io.ReadAll(r) - if err != nil { - return nil, fmt.Errorf("read data from GCS object %s/%s: %w", s.bucket.BucketName(), key, err) - } - - return data, nil -} - -func (s *GCSStorage) Exists(key string) (bool, error) { - ctx := context.TODO() - obj := s.bucket.Object(key) - _, err := obj.Attrs(ctx) - if err == nil { - return true, nil - } - if errors.Is(err, storage.ErrObjectNotExist) { - return false, nil - } - return false, fmt.Errorf("check existence of GCS object %s/%s: %w", s.bucket.BucketName(), key, err) -} - -func (s *GCSStorage) State(key string) (oss.OSSState, error) { - ctx := context.TODO() - obj := s.bucket.Object(key) - - attrs, err := obj.Attrs(ctx) - if err != nil { - return oss.OSSState{}, fmt.Errorf("get attributes of GCS object %s/%s: %w", s.bucket.BucketName(), key, err) - } - - state := oss.OSSState{ - Size: attrs.Size, - LastModified: attrs.Updated, - } - return state, nil -} - -func (s *GCSStorage) List(prefix string) ([]oss.OSSPath, error) { - ctx := context.TODO() - paths := make([]oss.OSSPath, 0) - // NOTE: Query prefix must be empty when listing from the root - if prefix == "/" { - prefix = "" - } - query := &storage.Query{Prefix: prefix} - - it := s.bucket.Objects(ctx, query) - for { - fmt.Println("iterating over GCS objects with prefix:", prefix) - attrs, err := it.Next() - if errors.Is(err, iterator.Done) { - break - } - if err != nil { - return nil, fmt.Errorf("list GCS objects with prefix %s: %w", prefix, err) - } - - // Skip if it's the prefix itself - if attrs.Name == prefix { - continue - } - - // remove prefix and leading slash - key := strings.TrimPrefix(attrs.Name, prefix) - key = strings.TrimPrefix(key, "/") - - paths = append(paths, oss.OSSPath{ - Path: key, - IsDir: false, - }) - } - - return paths, nil -} - -func (s *GCSStorage) Delete(key string) error { - ctx := context.TODO() - err := s.bucket.Object(key).Delete(ctx) - if err != nil { - return fmt.Errorf("delete GCS object %s/%s: %w", s.bucket.BucketName(), key, err) - } - return nil -} diff --git a/internal/oss/gcs/gcs_storage_test.go b/internal/oss/gcs/gcs_storage_test.go deleted file mode 100644 index 2b380cae47..0000000000 --- a/internal/oss/gcs/gcs_storage_test.go +++ /dev/null @@ -1,223 +0,0 @@ -package gcs_test - -import ( - "context" - "testing" - - "github.com/fsouza/fake-gcs-server/fakestorage" - "github.com/google/uuid" - "github.com/langgenius/dify-plugin-daemon/internal/oss" - "github.com/langgenius/dify-plugin-daemon/internal/oss/gcs" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/oauth2/google" - "google.golang.org/api/option" -) - -func getRandomBucketName(t *testing.T) string { - t.Helper() - bucketName := "test-bucket-" + uuid.NewString() - return bucketName -} - -func setupTestGCS(t *testing.T, bucketName string, initialObjects []fakestorage.Object) *gcs.GCSStorage { - t.Helper() - ctx := context.Background() - // Create the bucket - fakeServer.CreateBucketWithOpts( - fakestorage.CreateBucketOpts{ - Name: bucketName, - }, - ) - // Create initial objects if provided - for _, obj := range initialObjects { - require.Equal(t, obj.ObjectAttrs.BucketName, bucketName, "Object must belong to the created bucket") - fakeServer.CreateObject(obj) - } - - // Create the GCSStorage instance using the fake server's endpoint - storageInstance, err := gcs.NewGCSStorage(ctx, bucketName, option.WithHTTPClient(fakeServer.HTTPClient()), option.WithCredentials(&google.Credentials{})) - require.NoError(t, err, "Failed to create GCSStorage instance") - - return storageInstance -} - -func TestGCSStorage_Type(t *testing.T) { - bucketName := getRandomBucketName(t) - storageInstance := setupTestGCS(t, bucketName, []fakestorage.Object{}) - assert.Equal(t, oss.OSS_TYPE_GCS, storageInstance.Type()) -} - -func TestGCSStorage_Load(t *testing.T) { - bucketName := getRandomBucketName(t) - storageInstance := setupTestGCS(t, bucketName, []fakestorage.Object{ - { - ObjectAttrs: fakestorage.ObjectAttrs{ - Name: "file1.txt", - BucketName: bucketName, - }, - Content: []byte("file1"), - }, - }) - - actual, err := storageInstance.Load("file1.txt") - require.NoError(t, err, "Load should succeed for existing file") - assert.Equal(t, "file1", string(actual)) -} - -func TestGCSStorage_Exists(t *testing.T) { - bucketName := getRandomBucketName(t) - storageInstance := setupTestGCS(t, bucketName, []fakestorage.Object{ - { - ObjectAttrs: fakestorage.ObjectAttrs{ - Name: "file1.txt", - BucketName: bucketName, - }, - Content: []byte("file1"), - }, - }) - - tests := map[string]struct { - key string - expected bool - }{ - "FileExists": { - key: "file1.txt", - expected: true, - }, - "FileDoesNotExist": { - key: "non_existent_file.txt", - expected: false, - }, - } - - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - exists, err := storageInstance.Exists(tt.key) - require.NoError(t, err) - assert.Equal(t, tt.expected, exists) - }) - } -} - -func TestGCSStorage_State(t *testing.T) { - bucketName := getRandomBucketName(t) - storageInstance := setupTestGCS(t, bucketName, []fakestorage.Object{ - { - ObjectAttrs: fakestorage.ObjectAttrs{ - Name: "file1.txt", - BucketName: bucketName, - }, - Content: []byte("file1"), - }, - }) - - state, err := storageInstance.State("file1.txt") - require.NoError(t, err, "State should succeed for existing file") - assert.Greater(t, state.Size, int64(0), "File size should be greater than 0") - assert.NotZero(t, state.LastModified, "Last modified time should not be zero") -} - -func TestGCSStorage_List(t *testing.T) { - bucketName := getRandomBucketName(t) - storageInstance := setupTestGCS(t, bucketName, []fakestorage.Object{ - { - ObjectAttrs: fakestorage.ObjectAttrs{ - Name: "file1.txt", - BucketName: bucketName, - }, - Content: []byte("file1"), - }, - { - ObjectAttrs: fakestorage.ObjectAttrs{ - Name: "dir1/file2.txt", - BucketName: bucketName, - }, - Content: []byte("file2"), - }, - { - ObjectAttrs: fakestorage.ObjectAttrs{ - Name: "dir1/subdir/file3.txt", - BucketName: bucketName, - }, - Content: []byte("file3"), - }, - { - ObjectAttrs: fakestorage.ObjectAttrs{ - Name: "dir2/file4.txt", - BucketName: bucketName, - }, - Content: []byte("file4"), - }, - }) - - tests := map[string]struct { - prefix string - expected []oss.OSSPath - }{ - "ListRootDirectoryWithoutSlask": { - prefix: "", - expected: []oss.OSSPath{ - {Path: "file1.txt", IsDir: false}, - {Path: "dir1/file2.txt", IsDir: false}, - {Path: "dir1/subdir/file3.txt", IsDir: false}, - {Path: "dir2/file4.txt", IsDir: false}, - }, - }, - "ListRootDirectoryWithSlash": { - prefix: "/", - expected: []oss.OSSPath{ - {Path: "file1.txt", IsDir: false}, - {Path: "dir1/file2.txt", IsDir: false}, - {Path: "dir1/subdir/file3.txt", IsDir: false}, - {Path: "dir2/file4.txt", IsDir: false}, - }, - }, - "ListDirectoryWithSlash": { - prefix: "dir1/", - expected: []oss.OSSPath{ - {Path: "file2.txt", IsDir: false}, - {Path: "subdir/file3.txt", IsDir: false}, - }, - }, - "ListDirectoryWithoutSlash": { - prefix: "dir1", - expected: []oss.OSSPath{ - {Path: "file2.txt", IsDir: false}, - {Path: "subdir/file3.txt", IsDir: false}, - }, - }, - "ListNonExistentDirectory": { - prefix: "non_existent_dir/", - expected: []oss.OSSPath{}, - }, - } - - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - actualRoot, err := storageInstance.List(tt.prefix) - require.NoError(t, err, "List with prefix should succeed") - assert.ElementsMatch(t, tt.expected, actualRoot, "List(\"%s\") should return expected files and dirs", tt.prefix) - }) - } -} - -func TestGCSStorage_Delete(t *testing.T) { - bucketName := getRandomBucketName(t) - storageInstance := setupTestGCS(t, bucketName, []fakestorage.Object{ - { - ObjectAttrs: fakestorage.ObjectAttrs{ - Name: "file_to_delete.txt", - BucketName: bucketName, - }, - Content: []byte("file to be deleted"), - }, - }) - - err := storageInstance.Delete("file_to_delete.txt") - require.NoError(t, err, "Delete should succeed for existing file") - // Verify file doesn't exist after deletion - exists, err := storageInstance.Exists("file_to_delete.txt") - require.NoError(t, err) - assert.False(t, exists, "File should not exist after deletion") -} diff --git a/internal/oss/gcs/main_test.go b/internal/oss/gcs/main_test.go deleted file mode 100644 index c41ef02cd5..0000000000 --- a/internal/oss/gcs/main_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package gcs_test - -import ( - "os" - "testing" - - "github.com/fsouza/fake-gcs-server/fakestorage" -) - -const ( - gcsTestHost = "127.0.0.1" - gcsTestPort = 8081 -) - -var ( - fakeServer *fakestorage.Server -) - -func TestMain(m *testing.M) { - server, err := fakestorage.NewServerWithOptions(fakestorage.Options{ - Host: gcsTestHost, - Port: gcsTestPort, - Scheme: "http", - }) - if err != nil { - panic(err) - } - - fakeServer = server - exitCode := m.Run() - - fakeServer.Stop() - os.Exit(exitCode) -} diff --git a/internal/oss/local/local_storage.go b/internal/oss/local/local_storage.go deleted file mode 100644 index 958ad90e09..0000000000 --- a/internal/oss/local/local_storage.go +++ /dev/null @@ -1,104 +0,0 @@ -package local - -import ( - "io/fs" - "log" - "os" - "path/filepath" - "strings" - - "github.com/langgenius/dify-plugin-daemon/internal/oss" -) - -type LocalStorage struct { - root string -} - -func NewLocalStorage(root string) oss.OSS { - if err := os.MkdirAll(root, 0o755); err != nil { - log.Panicf("Failed to create storage path: %s", err) - } - - return &LocalStorage{root: root} -} - -func (l *LocalStorage) Save(key string, data []byte) error { - path := filepath.Join(l.root, key) - filePath := filepath.Dir(path) - if err := os.MkdirAll(filePath, 0o755); err != nil { - return err - } - - return os.WriteFile(path, data, 0o644) -} - -func (l *LocalStorage) Load(key string) ([]byte, error) { - path := filepath.Join(l.root, key) - - return os.ReadFile(path) -} - -func (l *LocalStorage) Exists(key string) (bool, error) { - path := filepath.Join(l.root, key) - - _, err := os.Stat(path) - return err == nil, nil -} - -func (l *LocalStorage) State(key string) (oss.OSSState, error) { - path := filepath.Join(l.root, key) - - info, err := os.Stat(path) - if err != nil { - return oss.OSSState{}, err - } - - return oss.OSSState{Size: info.Size(), LastModified: info.ModTime()}, nil -} - -func (l *LocalStorage) List(prefix string) ([]oss.OSSPath, error) { - paths := make([]oss.OSSPath, 0) - // check if the patch exists - exists, err := l.Exists(prefix) - if err != nil { - return nil, err - } - if !exists { - return paths, nil - } - prefix = filepath.Join(l.root, prefix) - - err = filepath.WalkDir(prefix, func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - // remove prefix - path = strings.TrimPrefix(path, prefix) - if path == "" { - return nil - } - // remove leading slash - path = strings.TrimPrefix(path, "/") - paths = append(paths, oss.OSSPath{ - Path: path, - IsDir: d.IsDir(), - }) - return nil - }) - - if err != nil { - return nil, err - } - - return paths, nil -} - -func (l *LocalStorage) Delete(key string) error { - path := filepath.Join(l.root, key) - - return os.RemoveAll(path) -} - -func (l *LocalStorage) Type() string { - return oss.OSS_TYPE_LOCAL -} \ No newline at end of file diff --git a/internal/oss/s3/s3_storage.go b/internal/oss/s3/s3_storage.go deleted file mode 100644 index ca2b46508b..0000000000 --- a/internal/oss/s3/s3_storage.go +++ /dev/null @@ -1,184 +0,0 @@ -package s3 - -import ( - "bytes" - "context" - "io" - "strings" - "time" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/credentials" - "github.com/aws/aws-sdk-go-v2/service/s3" - "github.com/langgenius/dify-plugin-daemon/internal/oss" - "github.com/langgenius/dify-plugin-daemon/internal/utils/parser" -) - -type S3Storage struct { - bucket string - client *s3.Client -} - -func NewS3Storage(useAws bool, endpoint string, usePathStyle bool, ak string, sk string, bucket string, region string) (oss.OSS, error) { - var cfg aws.Config - var err error - var client *s3.Client - - if useAws { - if ak == "" && sk == "" { - cfg, err = config.LoadDefaultConfig( - context.TODO(), - config.WithRegion(region), - ) - } else { - cfg, err = config.LoadDefaultConfig( - context.TODO(), - config.WithRegion(region), - config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( - ak, - sk, - "", - )), - ) - } - if err != nil { - return nil, err - } - - client = s3.NewFromConfig(cfg, func(options *s3.Options) { - if endpoint != "" { - options.BaseEndpoint = aws.String(endpoint) - } - }) - } else { - client = s3.New(s3.Options{ - Credentials: credentials.NewStaticCredentialsProvider(ak, sk, ""), - UsePathStyle: usePathStyle, - Region: region, - EndpointResolver: s3.EndpointResolverFunc( - func(region string, options s3.EndpointResolverOptions) (aws.Endpoint, error) { - return aws.Endpoint{ - URL: endpoint, - HostnameImmutable: false, - SigningName: "s3", - PartitionID: "aws", - SigningRegion: region, - SigningMethod: "v4", - Source: aws.EndpointSourceCustom, - }, nil - }), - }) - } - - // check bucket - _, err = client.HeadBucket(context.TODO(), &s3.HeadBucketInput{ - Bucket: aws.String(bucket), - }) - if err != nil { - _, err = client.CreateBucket(context.TODO(), &s3.CreateBucketInput{ - Bucket: aws.String(bucket), - }) - if err != nil { - return nil, err - } - } - - return &S3Storage{bucket: bucket, client: client}, nil -} - -func (s *S3Storage) Save(key string, data []byte) error { - _, err := s.client.PutObject(context.TODO(), &s3.PutObjectInput{ - Bucket: aws.String(s.bucket), - Key: aws.String(key), - Body: bytes.NewReader(data), - }) - return err -} - -func (s *S3Storage) Load(key string) ([]byte, error) { - resp, err := s.client.GetObject(context.TODO(), &s3.GetObjectInput{ - Bucket: aws.String(s.bucket), - Key: aws.String(key), - }) - if err != nil { - return nil, err - } - - return io.ReadAll(resp.Body) -} - -func (s *S3Storage) Exists(key string) (bool, error) { - _, err := s.client.HeadObject(context.TODO(), &s3.HeadObjectInput{ - Bucket: aws.String(s.bucket), - Key: aws.String(key), - }) - return err == nil, nil -} - -func (s *S3Storage) Delete(key string) error { - _, err := s.client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{ - Bucket: aws.String(s.bucket), - Key: aws.String(key), - }) - return err -} - -func (s *S3Storage) List(prefix string) ([]oss.OSSPath, error) { - // append a slash to the prefix if it doesn't end with one - if !strings.HasSuffix(prefix, "/") { - prefix = prefix + "/" - } - - var keys []oss.OSSPath - input := &s3.ListObjectsV2Input{ - Bucket: aws.String(s.bucket), - Prefix: aws.String(prefix), - } - - paginator := s3.NewListObjectsV2Paginator(s.client, input) - for paginator.HasMorePages() { - page, err := paginator.NextPage(context.TODO()) - if err != nil { - return nil, err - } - for _, obj := range page.Contents { - // remove prefix - key := strings.TrimPrefix(*obj.Key, prefix) - // remove leading slash - key = strings.TrimPrefix(key, "/") - keys = append(keys, oss.OSSPath{ - Path: key, - IsDir: false, - }) - } - } - - return keys, nil -} - -func (s *S3Storage) State(key string) (oss.OSSState, error) { - resp, err := s.client.HeadObject(context.TODO(), &s3.HeadObjectInput{ - Bucket: aws.String(s.bucket), - Key: aws.String(key), - }) - if err != nil { - return oss.OSSState{}, err - } - - if resp.ContentLength == nil { - resp.ContentLength = parser.ToPtr[int64](0) - } - if resp.LastModified == nil { - resp.LastModified = parser.ToPtr(time.Time{}) - } - - return oss.OSSState{ - Size: *resp.ContentLength, - LastModified: *resp.LastModified, - }, nil -} - -func (s *S3Storage) Type() string { - return oss.OSS_TYPE_S3 -} diff --git a/internal/oss/tencent_cos/tencent_cos_storage.go b/internal/oss/tencent_cos/tencent_cos_storage.go deleted file mode 100644 index e4fe77e941..0000000000 --- a/internal/oss/tencent_cos/tencent_cos_storage.go +++ /dev/null @@ -1,165 +0,0 @@ -package tencent_cos - -import ( - "bytes" - "context" - "io" - "net/http" - "net/url" - "strings" - "time" - - "github.com/langgenius/dify-plugin-daemon/internal/oss" - "github.com/tencentyun/cos-go-sdk-v5" -) - -type TencentCOSStorage struct { - bucket string - region string - client *cos.Client -} - -func NewTencentCOSStorage(secretID string, secretKey string, region string, bucket string) (oss.OSS, error) { - u, err := url.Parse("https://" + bucket + ".cos." + region + ".myqcloud.com") - if err != nil { - return nil, err - } - - b := &cos.BaseURL{BucketURL: u} - client := cos.NewClient(b, &http.Client{ - Transport: &cos.AuthorizationTransport{ - SecretID: secretID, - SecretKey: secretKey, - }, - }) - - _, err = client.Bucket.Head(context.Background()) - if err != nil { - return nil, err - } - - return &TencentCOSStorage{ - bucket: bucket, - region: region, - client: client, - }, nil -} - -func (s *TencentCOSStorage) Save(key string, data []byte) error { - _, err := s.client.Object.Put(context.Background(), key, bytes.NewReader(data), nil) - return err -} - -func (s *TencentCOSStorage) Load(key string) ([]byte, error) { - resp, err := s.client.Object.Get(context.Background(), key, nil) - if err != nil { - return nil, err - } - - return io.ReadAll(resp.Body) -} - -func (s *TencentCOSStorage) Exists(key string) (bool, error) { - ok, err := s.client.Object.IsExist(context.Background(), key) - if err == nil && ok { - return true, nil - } else if err != nil { - return false, err - } else { - return false, nil - } -} - -func (s *TencentCOSStorage) Delete(key string) error { - _, err := s.client.Object.Delete(context.Background(), key) - return err -} - -func (s *TencentCOSStorage) List(prefix string) ([]oss.OSSPath, error) { - if !strings.HasSuffix(prefix, "/") { - prefix = prefix + "/" - } - - var keys []oss.OSSPath - opt := &cos.BucketGetOptions{ - Prefix: prefix, - Delimiter: "/", - } - isTruncated := true - var marker string - for isTruncated { - if marker != "" { - opt.Marker = marker - } - - result, _, err := s.client.Bucket.Get(context.Background(), opt) - if err != nil { - return nil, err - } - - for _, content := range result.Contents { - // remove prefix - key := strings.TrimPrefix(content.Key, prefix) - // remove leading slash - key = strings.TrimPrefix(key, "/") - if key == "" { - continue - } - keys = append(keys, oss.OSSPath{ - Path: key, - IsDir: false, - }) - } - - for _, commonPrefix := range result.CommonPrefixes { - if commonPrefix == "" { - continue - } - if !strings.HasSuffix(commonPrefix, "/") { - commonPrefix = commonPrefix + "/" - } - keys = append(keys, oss.OSSPath{ - Path: commonPrefix, - IsDir: true, - }) - - subKeys, _ := s.List(commonPrefix) - if len(subKeys) > 0 { - subPrefix := strings.TrimPrefix(commonPrefix, prefix) - for i := range subKeys { - subKeys[i].Path = subPrefix + subKeys[i].Path - } - keys = append(keys, subKeys...) - } - - } - - isTruncated = result.IsTruncated - marker = result.NextMarker - } - - return keys, nil -} - -func (s *TencentCOSStorage) State(key string) (oss.OSSState, error) { - resp, err := s.client.Object.Head(context.Background(), key, nil) - if err != nil { - return oss.OSSState{}, err - } - - contentLength := resp.ContentLength - - lastModified, err := time.Parse(time.RFC1123, resp.Header.Get("Last-Modified")) - if err != nil { - lastModified = time.Time{} - } - - return oss.OSSState{ - Size: contentLength, - LastModified: lastModified, - }, nil -} - -func (s *TencentCOSStorage) Type() string { - return oss.OSS_TYPE_TENCENT_COS -} diff --git a/internal/oss/type.go b/internal/oss/type.go deleted file mode 100644 index 130c8ecc92..0000000000 --- a/internal/oss/type.go +++ /dev/null @@ -1,43 +0,0 @@ -package oss - -import "time" - -// OSS supports different types of object storage services -// such as local file system, AWS S3, and Tencent COS. -// The interface defines methods for saving, loading, checking existence, -const ( - OSS_TYPE_LOCAL = "local" - OSS_TYPE_S3 = "aws_s3" - OSS_TYPE_TENCENT_COS = "tencent_cos" - OSS_TYPE_AZURE_BLOB = "azure_blob" - OSS_TYPE_GCS = "gcs" - OSS_TYPE_ALIYUN_OSS = "aliyun_oss" -) - -type OSSState struct { - Size int64 - LastModified time.Time -} - -type OSSPath struct { - Path string - IsDir bool -} - -type OSS interface { - // Save saves data into path key - Save(key string, data []byte) error - // Load loads data from path key - Load(key string) ([]byte, error) - // Exists checks if the data exists in the path key - Exists(key string) (bool, error) - // State gets the state of the data in the path key - State(key string) (OSSState, error) - // List lists all the data with the given prefix, and all the paths are absolute paths - List(prefix string) ([]OSSPath, error) - // Delete deletes the data in the path key - Delete(key string) error - // Type returns the type of the storage - // For example: local, aws_s3, tencent_cos - Type() string -} diff --git a/internal/server/controllers/agent.go b/internal/server/controllers/agent.go index d64c46f357..5886d47b79 100644 --- a/internal/server/controllers/agent.go +++ b/internal/server/controllers/agent.go @@ -5,24 +5,8 @@ import ( "github.com/gin-gonic/gin" "github.com/langgenius/dify-plugin-daemon/internal/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/entities/requests" ) -func InvokeAgentStrategy(config *app.Config) gin.HandlerFunc { - type request = plugin_entities.InvokePluginRequest[requests.RequestInvokeAgentStrategy] - - return func(c *gin.Context) { - BindPluginDispatchRequest( - c, - func(itr request) { - service.InvokeAgentStrategy(&itr, c, config.PluginMaxExecutionTimeout) - }, - ) - } -} - func ListAgentStrategies(c *gin.Context) { BindRequest(c, func(request struct { TenantID string `uri:"tenant_id" validate:"required"` diff --git a/internal/server/controllers/agent_strategy.gen.go b/internal/server/controllers/agent_strategy.gen.go new file mode 100644 index 0000000000..223fbb9ad4 --- /dev/null +++ b/internal/server/controllers/agent_strategy.gen.go @@ -0,0 +1,24 @@ +// Code generated by controller generator. DO NOT EDIT. + +package controllers + +import ( + "github.com/gin-gonic/gin" + "github.com/langgenius/dify-plugin-daemon/internal/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/entities/requests" +) + +func InvokeAgentStrategy(config *app.Config) gin.HandlerFunc { + type request = plugin_entities.InvokePluginRequest[requests.RequestInvokeAgentStrategy] + + return func(c *gin.Context) { + BindPluginDispatchRequest( + c, + func(itr request) { + service.InvokeAgentStrategy(&itr, c, config.PluginMaxExecutionTimeout) + }, + ) + } +} diff --git a/internal/server/controllers/config/definitions.go b/internal/server/controllers/config/definitions.go new file mode 100644 index 0000000000..0eb8d3f9e8 --- /dev/null +++ b/internal/server/controllers/config/definitions.go @@ -0,0 +1,40 @@ +package config + +import ( + "github.com/langgenius/dify-plugin-daemon/pkg/entities/model_entities" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/requests" +) + +// PluginInvoker defines a plugin invocation controller configuration +type PluginInvoker struct { + // Name is the name of the controller function + Name string + // RequestType is the request type for this controller + RequestType any + // ResponseType is the response type for this controller + ResponseType any + // ResponseTypeName is the name of the response type + ResponseTypeName string + // BufferSize is the size of the response buffer + BufferSize int +} + +// PluginInvokers is a map of plugin invoker configurations +var PluginInvokers = map[string]PluginInvoker{ + "InvokeLLM": { + Name: "InvokeLLM", + RequestType: plugin_entities.InvokePluginRequest[requests.RequestInvokeLLM]{}, + ResponseType: model_entities.LLMResultChunk{}, + ResponseTypeName: "LLMResultChunk", + BufferSize: 512, + }, + "InvokeTextEmbedding": { + Name: "InvokeTextEmbedding", + RequestType: plugin_entities.InvokePluginRequest[requests.RequestInvokeTextEmbedding]{}, + ResponseType: model_entities.TextEmbeddingResult{}, + ResponseTypeName: "TextEmbeddingResult", + BufferSize: 1, + }, + // ... other plugin invokers +} diff --git a/internal/server/controllers/controllers.go b/internal/server/controllers/controllers.go new file mode 100644 index 0000000000..9555d8d517 --- /dev/null +++ b/internal/server/controllers/controllers.go @@ -0,0 +1,3 @@ +//go:generate go run ../../cmd/codegen/main.go + +package controllers diff --git a/internal/server/controllers/definitions/definitions.go b/internal/server/controllers/definitions/definitions.go new file mode 100644 index 0000000000..a4a3b0d2cc --- /dev/null +++ b/internal/server/controllers/definitions/definitions.go @@ -0,0 +1,249 @@ +package definitions + +import ( + "github.com/langgenius/dify-plugin-daemon/internal/core/plugin_daemon/access_types" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/dynamic_select_entities" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/model_entities" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/oauth_entities" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/requests" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/tool_entities" +) + +// PluginDispatcher defines a plugin dispatcher +type PluginDispatcher struct { + Name string + RequestType interface{} // e.g. requests.RequestInvokeLLM + ResponseType interface{} // e.g. requests.ResponseInvokeLLM + RequestTypeString string + ResponseTypeString string + AccessType access_types.PluginAccessType + AccessAction access_types.PluginAccessAction + AccessTypeString string + AccessActionString string + BufferSize int + Path string // e.g. "/tool/invoke" +} + +// Define all plugin dispatchers +var PluginDispatchers = []PluginDispatcher{ + // { // No need to implement this for now, it has its special implementation in the agent service + // Name: "InvokeTool", + // RequestType: requests.RequestInvokeTool{}, + // ResponseType: tool_entities.ToolResponseChunk{}, + // AccessType: access_types.PLUGIN_ACCESS_TYPE_TOOL, + // AccessAction: access_types.PLUGIN_ACCESS_ACTION_INVOKE_TOOL, + // AccessTypeString: "access_types.PLUGIN_ACCESS_TYPE_TOOL", + // AccessActionString: "access_types.PLUGIN_ACCESS_ACTION_INVOKE_TOOL", + // BufferSize: 1024, + // Path: "/tool/invoke", + // }, + { + Name: "ValidateToolCredentials", + RequestType: requests.RequestValidateToolCredentials{}, + ResponseType: tool_entities.ValidateCredentialsResult{}, + AccessType: access_types.PLUGIN_ACCESS_TYPE_TOOL, + AccessAction: access_types.PLUGIN_ACCESS_ACTION_VALIDATE_TOOL_CREDENTIALS, + AccessTypeString: "access_types.PLUGIN_ACCESS_TYPE_TOOL", + AccessActionString: "access_types.PLUGIN_ACCESS_ACTION_VALIDATE_TOOL_CREDENTIALS", + BufferSize: 1, + Path: "/tool/validate_credentials", + }, + { + Name: "GetToolRuntimeParameters", + RequestType: requests.RequestGetToolRuntimeParameters{}, + ResponseType: tool_entities.GetToolRuntimeParametersResponse{}, + AccessType: access_types.PLUGIN_ACCESS_TYPE_TOOL, + AccessAction: access_types.PLUGIN_ACCESS_ACTION_GET_TOOL_RUNTIME_PARAMETERS, + AccessTypeString: "access_types.PLUGIN_ACCESS_TYPE_TOOL", + AccessActionString: "access_types.PLUGIN_ACCESS_ACTION_GET_TOOL_RUNTIME_PARAMETERS", + BufferSize: 1, + Path: "/tool/get_runtime_parameters", + }, + { + Name: "InvokeLLM", + RequestType: requests.RequestInvokeLLM{}, + ResponseType: model_entities.LLMResultChunk{}, + AccessType: access_types.PLUGIN_ACCESS_TYPE_MODEL, + AccessAction: access_types.PLUGIN_ACCESS_ACTION_INVOKE_LLM, + AccessTypeString: "access_types.PLUGIN_ACCESS_TYPE_MODEL", + AccessActionString: "access_types.PLUGIN_ACCESS_ACTION_INVOKE_LLM", + BufferSize: 512, + Path: "/llm/invoke", + }, + { + Name: "GetLLMNumTokens", + RequestType: requests.RequestGetLLMNumTokens{}, + ResponseType: model_entities.LLMGetNumTokensResponse{}, + AccessType: access_types.PLUGIN_ACCESS_TYPE_MODEL, + AccessAction: access_types.PLUGIN_ACCESS_ACTION_GET_LLM_NUM_TOKENS, + AccessTypeString: "access_types.PLUGIN_ACCESS_TYPE_MODEL", + AccessActionString: "access_types.PLUGIN_ACCESS_ACTION_GET_LLM_NUM_TOKENS", + BufferSize: 1, + Path: "/llm/num_tokens", + }, + { + Name: "InvokeTextEmbedding", + RequestType: requests.RequestInvokeTextEmbedding{}, + ResponseType: model_entities.TextEmbeddingResult{}, + AccessType: access_types.PLUGIN_ACCESS_TYPE_MODEL, + AccessAction: access_types.PLUGIN_ACCESS_ACTION_INVOKE_TEXT_EMBEDDING, + AccessTypeString: "access_types.PLUGIN_ACCESS_TYPE_MODEL", + AccessActionString: "access_types.PLUGIN_ACCESS_ACTION_INVOKE_TEXT_EMBEDDING", + BufferSize: 1, + Path: "/text_embedding/invoke", + }, + { + Name: "GetTextEmbeddingNumTokens", + RequestType: requests.RequestGetTextEmbeddingNumTokens{}, + ResponseType: model_entities.GetTextEmbeddingNumTokensResponse{}, + AccessType: access_types.PLUGIN_ACCESS_TYPE_MODEL, + AccessAction: access_types.PLUGIN_ACCESS_ACTION_GET_TEXT_EMBEDDING_NUM_TOKENS, + AccessTypeString: "access_types.PLUGIN_ACCESS_TYPE_MODEL", + AccessActionString: "access_types.PLUGIN_ACCESS_ACTION_GET_TEXT_EMBEDDING_NUM_TOKENS", + BufferSize: 1, + Path: "/text_embedding/num_tokens", + }, + { + Name: "InvokeRerank", + RequestType: requests.RequestInvokeRerank{}, + ResponseType: model_entities.RerankResult{}, + AccessType: access_types.PLUGIN_ACCESS_TYPE_MODEL, + AccessAction: access_types.PLUGIN_ACCESS_ACTION_INVOKE_RERANK, + AccessTypeString: "access_types.PLUGIN_ACCESS_TYPE_MODEL", + AccessActionString: "access_types.PLUGIN_ACCESS_ACTION_INVOKE_RERANK", + BufferSize: 1, + Path: "/rerank/invoke", + }, + { + Name: "InvokeTTS", + RequestType: requests.RequestInvokeTTS{}, + ResponseType: model_entities.TTSResult{}, + AccessType: access_types.PLUGIN_ACCESS_TYPE_MODEL, + AccessAction: access_types.PLUGIN_ACCESS_ACTION_INVOKE_TTS, + AccessTypeString: "access_types.PLUGIN_ACCESS_TYPE_MODEL", + AccessActionString: "access_types.PLUGIN_ACCESS_ACTION_INVOKE_TTS", + BufferSize: 512, + Path: "/tts/invoke", + }, + { + Name: "GetTTSModelVoices", + RequestType: requests.RequestGetTTSModelVoices{}, + ResponseType: model_entities.GetTTSVoicesResponse{}, + AccessType: access_types.PLUGIN_ACCESS_TYPE_MODEL, + AccessAction: access_types.PLUGIN_ACCESS_ACTION_GET_TTS_MODEL_VOICES, + AccessTypeString: "access_types.PLUGIN_ACCESS_TYPE_MODEL", + AccessActionString: "access_types.PLUGIN_ACCESS_ACTION_GET_TTS_MODEL_VOICES", + BufferSize: 1, + Path: "/tts/model/voices", + }, + { + Name: "InvokeSpeech2Text", + RequestType: requests.RequestInvokeSpeech2Text{}, + ResponseType: model_entities.Speech2TextResult{}, + AccessType: access_types.PLUGIN_ACCESS_TYPE_MODEL, + AccessAction: access_types.PLUGIN_ACCESS_ACTION_INVOKE_SPEECH2TEXT, + AccessTypeString: "access_types.PLUGIN_ACCESS_TYPE_MODEL", + AccessActionString: "access_types.PLUGIN_ACCESS_ACTION_INVOKE_SPEECH2TEXT", + BufferSize: 1, + Path: "/speech2text/invoke", + }, + { + Name: "InvokeModeration", + RequestType: requests.RequestInvokeModeration{}, + ResponseType: model_entities.ModerationResult{}, + AccessType: access_types.PLUGIN_ACCESS_TYPE_MODEL, + AccessAction: access_types.PLUGIN_ACCESS_ACTION_INVOKE_MODERATION, + AccessTypeString: "access_types.PLUGIN_ACCESS_TYPE_MODEL", + AccessActionString: "access_types.PLUGIN_ACCESS_ACTION_INVOKE_MODERATION", + BufferSize: 1, + Path: "/moderation/invoke", + }, + { + Name: "ValidateProviderCredentials", + RequestType: requests.RequestValidateProviderCredentials{}, + ResponseType: model_entities.ValidateCredentialsResult{}, + AccessType: access_types.PLUGIN_ACCESS_TYPE_MODEL, + AccessAction: access_types.PLUGIN_ACCESS_ACTION_VALIDATE_PROVIDER_CREDENTIALS, + AccessTypeString: "access_types.PLUGIN_ACCESS_TYPE_MODEL", + AccessActionString: "access_types.PLUGIN_ACCESS_ACTION_VALIDATE_PROVIDER_CREDENTIALS", + BufferSize: 1, + Path: "/model/validate_provider_credentials", + }, + { + Name: "ValidateModelCredentials", + RequestType: requests.RequestValidateModelCredentials{}, + ResponseType: model_entities.ValidateCredentialsResult{}, + AccessType: access_types.PLUGIN_ACCESS_TYPE_MODEL, + AccessAction: access_types.PLUGIN_ACCESS_ACTION_VALIDATE_MODEL_CREDENTIALS, + AccessTypeString: "access_types.PLUGIN_ACCESS_TYPE_MODEL", + AccessActionString: "access_types.PLUGIN_ACCESS_ACTION_VALIDATE_MODEL_CREDENTIALS", + BufferSize: 1, + Path: "/model/validate_model_credentials", + }, + { + Name: "GetAIModelSchema", + RequestType: requests.RequestGetAIModelSchema{}, + ResponseType: model_entities.GetModelSchemasResponse{}, + AccessType: access_types.PLUGIN_ACCESS_TYPE_MODEL, + AccessAction: access_types.PLUGIN_ACCESS_ACTION_GET_AI_MODEL_SCHEMAS, + AccessTypeString: "access_types.PLUGIN_ACCESS_TYPE_MODEL", + AccessActionString: "access_types.PLUGIN_ACCESS_ACTION_GET_AI_MODEL_SCHEMAS", + BufferSize: 1, + Path: "/model/schema", + }, + // { // No need to implement this for now, it has its special implementation in the agent service + // Name: "InvokeAgentStrategy", + // RequestType: requests.RequestInvokeAgentStrategy{}, + // ResponseType: agent_entities.AgentStrategyResponseChunk{}, + // AccessType: access_types.PLUGIN_ACCESS_TYPE_AGENT_STRATEGY, + // AccessAction: access_types.PLUGIN_ACCESS_ACTION_INVOKE_AGENT_STRATEGY, + // AccessTypeString: "access_types.PLUGIN_ACCESS_TYPE_AGENT_STRATEGY", + // AccessActionString: "access_types.PLUGIN_ACCESS_ACTION_INVOKE_AGENT_STRATEGY", + // BufferSize: 512, + // Path: "/agent_strategy/invoke", + // }, + { + Name: "GetAuthorizationURL", + RequestType: requests.RequestOAuthGetAuthorizationURL{}, + ResponseType: oauth_entities.OAuthGetAuthorizationURLResult{}, + AccessType: access_types.PLUGIN_ACCESS_TYPE_OAUTH, + AccessAction: access_types.PLUGIN_ACCESS_ACTION_GET_AUTHORIZATION_URL, + AccessTypeString: "access_types.PLUGIN_ACCESS_TYPE_OAUTH", + AccessActionString: "access_types.PLUGIN_ACCESS_ACTION_GET_AUTHORIZATION_URL", + BufferSize: 1, + Path: "/oauth/get_authorization_url", + }, + { + Name: "GetCredentials", + RequestType: requests.RequestOAuthGetCredentials{}, + ResponseType: oauth_entities.OAuthGetCredentialsResult{}, + AccessType: access_types.PLUGIN_ACCESS_TYPE_OAUTH, + AccessAction: access_types.PLUGIN_ACCESS_ACTION_GET_CREDENTIALS, + AccessTypeString: "access_types.PLUGIN_ACCESS_TYPE_OAUTH", + AccessActionString: "access_types.PLUGIN_ACCESS_ACTION_GET_CREDENTIALS", + BufferSize: 1, + Path: "/oauth/get_credentials", + }, + { + Name: "RefreshCredentials", + RequestType: requests.RequestOAuthRefreshCredentials{}, + ResponseType: oauth_entities.OAuthRefreshCredentialsResult{}, + AccessType: access_types.PLUGIN_ACCESS_TYPE_OAUTH, + AccessAction: access_types.PLUGIN_ACCESS_ACTION_REFRESH_CREDENTIALS, + AccessTypeString: "access_types.PLUGIN_ACCESS_TYPE_OAUTH", + AccessActionString: "access_types.PLUGIN_ACCESS_ACTION_REFRESH_CREDENTIALS", + BufferSize: 1, + Path: "/oauth/refresh_credentials", + }, + { + Name: "FetchDynamicParameterOptions", + RequestType: requests.RequestDynamicParameterSelect{}, + ResponseType: dynamic_select_entities.DynamicSelectResult{}, + AccessType: access_types.PLUGIN_ACCESS_TYPE_DYNAMIC_PARAMETER, + AccessAction: access_types.PLUGIN_ACCESS_ACTION_DYNAMIC_PARAMETER_FETCH_OPTIONS, + AccessTypeString: "access_types.PLUGIN_ACCESS_TYPE_DYNAMIC_SELECT", + AccessActionString: "access_types.PLUGIN_ACCESS_ACTION_DYNAMIC_PARAMETER_FETCH_OPTIONS", + BufferSize: 1, + Path: "/dynamic_select/fetch_parameter_options", + }, +} diff --git a/internal/server/controllers/dynamic_select.gen.go b/internal/server/controllers/dynamic_select.gen.go new file mode 100644 index 0000000000..838bd5b5db --- /dev/null +++ b/internal/server/controllers/dynamic_select.gen.go @@ -0,0 +1,24 @@ +// Code generated by controller generator. DO NOT EDIT. + +package controllers + +import ( + "github.com/gin-gonic/gin" + "github.com/langgenius/dify-plugin-daemon/internal/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/entities/requests" +) + +func FetchDynamicParameterOptions(config *app.Config) gin.HandlerFunc { + type request = plugin_entities.InvokePluginRequest[requests.RequestDynamicParameterSelect] + + return func(c *gin.Context) { + BindPluginDispatchRequest( + c, + func(itr request) { + service.FetchDynamicParameterOptions(&itr, c, config.PluginMaxExecutionTimeout) + }, + ) + } +} diff --git a/internal/server/controllers/generator/generator.go b/internal/server/controllers/generator/generator.go new file mode 100644 index 0000000000..30085ddf2f --- /dev/null +++ b/internal/server/controllers/generator/generator.go @@ -0,0 +1,242 @@ +package generator + +import ( + "fmt" + "go/format" + "os" + "path/filepath" + "reflect" + "strings" + "text/template" + + "github.com/langgenius/dify-plugin-daemon/internal/core/plugin_daemon/access_types" + "github.com/langgenius/dify-plugin-daemon/internal/server/controllers/definitions" + "github.com/langgenius/dify-plugin-daemon/internal/utils/mapping" + "golang.org/x/tools/imports" +) + +// GenerateController generates a controller file for the given access type +func GenerateController(accessType access_types.PluginAccessType, dispatchers []*definitions.PluginDispatcher) error { + // Create template + tmpl := template.Must(template.New("controller").Parse(controllerTemplate)) + + // Create output file + outputPath := filepath.Join("internal", "server", "controllers", strings.ToLower(string(accessType))+".gen.go") + + // Execute template + var buf strings.Builder + if err := tmpl.Execute(&buf, struct { + AccessType access_types.PluginAccessType + Dispatchers []*definitions.PluginDispatcher + }{ + AccessType: accessType, + Dispatchers: dispatchers, + }); err != nil { + return fmt.Errorf("failed to execute template: %v", err) + } + + // Format code + src, err := format.Source([]byte(buf.String())) + if err != nil { + fmt.Println(buf.String()) + return fmt.Errorf("failed to format code: %v", err) + } + + // imports necessary packages + output, err := imports.Process(outputPath, src, nil) + if err != nil { + return fmt.Errorf("failed to process imports: %v", err) + } + + f, err := os.Create(outputPath) + if err != nil { + return fmt.Errorf("failed to create file: %v", err) + } + defer f.Close() + + // Write to file + if _, err := f.Write(output); err != nil { + return fmt.Errorf("failed to write file: %v", err) + } + + return nil +} + +// GenerateService generates a service file for the given access type +func GenerateService(accessType access_types.PluginAccessType, dispatchers []*definitions.PluginDispatcher) error { + // Create template + tmpl := template.Must(template.New("service").Parse(serviceTemplate)) + + // Create output file + outputPath := filepath.Join("internal", "service", strings.ToLower(string(accessType))+".gen.go") + + // Execute template + var buf strings.Builder + if err := tmpl.Execute(&buf, struct { + AccessType access_types.PluginAccessType + Dispatchers []*definitions.PluginDispatcher + }{ + AccessType: accessType, + Dispatchers: dispatchers, + }); err != nil { + return fmt.Errorf("failed to execute template: %v", err) + } + + // Format code + src, err := format.Source([]byte(buf.String())) + if err != nil { + return fmt.Errorf("failed to format code: %v", err) + } + + // imports necessary packages + output, err := imports.Process(outputPath, src, nil) + if err != nil { + return fmt.Errorf("failed to process imports: %v", err) + } + + f, err := os.Create(outputPath) + if err != nil { + return fmt.Errorf("failed to create file: %v", err) + } + defer f.Close() + + // Write to file + if _, err := f.Write(output); err != nil { + return fmt.Errorf("failed to write file: %v", err) + } + + return nil +} + +// GeneratePluginDaemon generates a plugin daemon file for the given access type +func GeneratePluginDaemon(accessType access_types.PluginAccessType, dispatchers []*definitions.PluginDispatcher) error { + // Create template + tmpl := template.Must(template.New("pluginDaemon").Parse(pluginDaemonTemplate)) + + // Create output file + outputPath := filepath.Join("internal", "core", "plugin_daemon", strings.ToLower(string(accessType))+".gen.go") + + // Execute template + var buf strings.Builder + if err := tmpl.Execute(&buf, struct { + AccessType access_types.PluginAccessType + Dispatchers []*definitions.PluginDispatcher + }{ + AccessType: accessType, + Dispatchers: dispatchers, + }); err != nil { + return fmt.Errorf("failed to execute template: %v", err) + } + + // Format code + src, err := format.Source([]byte(buf.String())) + if err != nil { + return fmt.Errorf("failed to format code: %v", err) + } + + // imports necessary packages + output, err := imports.Process(outputPath, src, nil) + if err != nil { + return fmt.Errorf("failed to process imports: %v", err) + } + + f, err := os.Create(outputPath) + if err != nil { + return fmt.Errorf("failed to create file: %v", err) + } + defer f.Close() + + // Write to file + if _, err := f.Write(output); err != nil { + return fmt.Errorf("failed to write file: %v", err) + } + + return nil +} + +// GenerateHTTPServer generates a http server file for the given access type +func GenerateHTTPServer(dispatchers []*definitions.PluginDispatcher) error { + // Create template + tmpl := template.Must(template.New("httpServer").Parse(httpServerTemplate)) + + // Create output file + outputPath := filepath.Join("internal", "server", "http_server.gen.go") + + // Execute template + var buf strings.Builder + if err := tmpl.Execute(&buf, struct { + Dispatchers []*definitions.PluginDispatcher + }{ + Dispatchers: dispatchers, + }); err != nil { + return fmt.Errorf("failed to execute template: %v", err) + } + + // Format code + src, err := format.Source([]byte(buf.String())) + if err != nil { + return fmt.Errorf("failed to format code: %v", err) + } + + // imports necessary packages + output, err := imports.Process(outputPath, src, nil) + if err != nil { + return fmt.Errorf("failed to process imports: %v", err) + } + + f, err := os.Create(outputPath) + if err != nil { + return fmt.Errorf("failed to create file: %v", err) + } + defer f.Close() + + // Write to file + if _, err := f.Write(output); err != nil { + return fmt.Errorf("failed to write file: %v", err) + } + + return nil +} + +// GenerateAll generates all controller and service files based on dispatchers +func GenerateAll() error { + // Group dispatchers by access type + dispatchersByType := make(map[access_types.PluginAccessType][]*definitions.PluginDispatcher) + for _, dispatcher := range definitions.PluginDispatchers { + dispatchersByType[dispatcher.AccessType] = append(dispatchersByType[dispatcher.AccessType], &dispatcher) + } + + // Override RequestType and ResponseType to be the actual type by using reflection + for _, dispatchers := range dispatchersByType { + for _, dispatcher := range dispatchers { + dispatcher.RequestTypeString = reflect.TypeOf(dispatcher.RequestType).String() + dispatcher.ResponseTypeString = reflect.TypeOf(dispatcher.ResponseType).String() + } + } + + // Generate files for each access type + for accessType, dispatchers := range dispatchersByType { + if err := GenerateController(accessType, dispatchers); err != nil { + return fmt.Errorf("failed to generate controller for %s: %v", accessType, err) + } + if err := GenerateService(accessType, dispatchers); err != nil { + return fmt.Errorf("failed to generate service for %s: %v", accessType, err) + } + if err := GeneratePluginDaemon(accessType, dispatchers); err != nil { + return fmt.Errorf("failed to generate plugin daemon for %s: %v", accessType, err) + } + } + + if err := GenerateHTTPServer( + mapping.MapArray( + definitions.PluginDispatchers, + func(dispatcher definitions.PluginDispatcher) *definitions.PluginDispatcher { + return &dispatcher + }, + ), + ); err != nil { + return fmt.Errorf("failed to generate http server: %v", err) + } + + return nil +} diff --git a/internal/server/controllers/generator/templates.go b/internal/server/controllers/generator/templates.go new file mode 100644 index 0000000000..fca37108e9 --- /dev/null +++ b/internal/server/controllers/generator/templates.go @@ -0,0 +1,108 @@ +package generator + +// controllerTemplate is the template for generating controller files +const controllerTemplate = `// Code generated by controller generator. DO NOT EDIT. + +package controllers + +import ( + "github.com/gin-gonic/gin" + "github.com/langgenius/dify-plugin-daemon/internal/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/entities/requests" +) + +{{range .Dispatchers}} +func {{.Name}}(config *app.Config) gin.HandlerFunc { + type request = plugin_entities.InvokePluginRequest[{{.RequestTypeString}}] + + return func(c *gin.Context) { + BindPluginDispatchRequest( + c, + func(itr request) { + service.{{.Name}}(&itr, c, config.PluginMaxExecutionTimeout) + }, + ) + } +} +{{end}} +` + +// serviceTemplate is the template for generating service files +const serviceTemplate = `// Code generated by controller generator. DO NOT EDIT. + +package service + +import ( + "github.com/gin-gonic/gin" + "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/session_manager" + "github.com/langgenius/dify-plugin-daemon/internal/utils/stream" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/requests" +) + +{{range .Dispatchers}} +func {{.Name}}( + r *plugin_entities.InvokePluginRequest[{{.RequestTypeString}}], + ctx *gin.Context, + max_timeout_seconds int, +) { + baseSSEWithSession( + func(session *session_manager.Session) (*stream.Stream[{{.ResponseTypeString}}], error) { + return plugin_daemon.{{.Name}}(session, &r.Data) + }, + {{.AccessTypeString}}, + {{.AccessActionString}}, + r, + ctx, + max_timeout_seconds, + ) +} +{{end}} +` + +// pluginDaemonTemplate is the template for generating plugin daemon files +const pluginDaemonTemplate = `// Code generated by controller generator. DO NOT EDIT. + +package plugin_daemon + +import ( + "github.com/langgenius/dify-plugin-daemon/internal/core/session_manager" + "github.com/langgenius/dify-plugin-daemon/internal/utils/stream" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/requests" +) + +{{range .Dispatchers}} +func {{.Name}}( + session *session_manager.Session, + request *{{.RequestTypeString}}, +) ( + *stream.Stream[{{.ResponseTypeString}}], error, +) { + return GenericInvokePlugin[{{.RequestTypeString}}, {{.ResponseTypeString}}]( + session, + request, + {{.BufferSize}}, + ) +} +{{end}} +` + +const httpServerTemplate = `// Code generated by controller generator. DO NOT EDIT. +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/langgenius/dify-plugin-daemon/internal/server/controllers" + "github.com/langgenius/dify-plugin-daemon/internal/types/app" +) + +func (app *App) setupGeneratedRoutes(group *gin.RouterGroup, config *app.Config) { + {{- range .Dispatchers}} + group.POST("{{.Path}}", controllers.{{.Name}}(config)) + {{- end}} +} +` diff --git a/internal/server/controllers/health_check.go b/internal/server/controllers/health_check.go index 06076636bb..e9978a206c 100644 --- a/internal/server/controllers/health_check.go +++ b/internal/server/controllers/health_check.go @@ -1,20 +1,45 @@ package controllers import ( + "sync/atomic" + "github.com/gin-gonic/gin" "github.com/langgenius/dify-plugin-daemon/internal/manifest" "github.com/langgenius/dify-plugin-daemon/internal/types/app" "github.com/langgenius/dify-plugin-daemon/internal/utils/routine" ) +var ( + activeRequests int32 = 0 // how many requests are active + activeDispatchRequests int32 = 0 // how many plugin dispatching requests are active +) + +func CollectActiveRequests() gin.HandlerFunc { + return func(ctx *gin.Context) { + atomic.AddInt32(&activeRequests, 1) + ctx.Next() + atomic.AddInt32(&activeRequests, -1) + } +} + +func CollectActiveDispatchRequests() gin.HandlerFunc { + return func(ctx *gin.Context) { + atomic.AddInt32(&activeDispatchRequests, 1) + ctx.Next() + atomic.AddInt32(&activeDispatchRequests, -1) + } +} + func HealthCheck(app *app.Config) gin.HandlerFunc { return func(c *gin.Context) { c.JSON(200, gin.H{ - "status": "ok", - "pool_status": routine.FetchRoutineStatus(), - "version": manifest.VersionX, - "build_time": manifest.BuildTimeX, - "platform": app.Platform, + "status": "ok", + "pool_status": routine.FetchRoutineStatus(), + "version": manifest.VersionX, + "build_time": manifest.BuildTimeX, + "platform": app.Platform, + "active_requests": activeRequests, + "active_dispatch_requests": activeDispatchRequests, }) } } diff --git a/internal/server/controllers/model.gen.go b/internal/server/controllers/model.gen.go new file mode 100644 index 0000000000..173045639e --- /dev/null +++ b/internal/server/controllers/model.gen.go @@ -0,0 +1,167 @@ +// Code generated by controller generator. DO NOT EDIT. + +package controllers + +import ( + "github.com/gin-gonic/gin" + "github.com/langgenius/dify-plugin-daemon/internal/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/entities/requests" +) + +func InvokeLLM(config *app.Config) gin.HandlerFunc { + type request = plugin_entities.InvokePluginRequest[requests.RequestInvokeLLM] + + return func(c *gin.Context) { + BindPluginDispatchRequest( + c, + func(itr request) { + service.InvokeLLM(&itr, c, config.PluginMaxExecutionTimeout) + }, + ) + } +} + +func GetLLMNumTokens(config *app.Config) gin.HandlerFunc { + type request = plugin_entities.InvokePluginRequest[requests.RequestGetLLMNumTokens] + + return func(c *gin.Context) { + BindPluginDispatchRequest( + c, + func(itr request) { + service.GetLLMNumTokens(&itr, c, config.PluginMaxExecutionTimeout) + }, + ) + } +} + +func InvokeTextEmbedding(config *app.Config) gin.HandlerFunc { + type request = plugin_entities.InvokePluginRequest[requests.RequestInvokeTextEmbedding] + + return func(c *gin.Context) { + BindPluginDispatchRequest( + c, + func(itr request) { + service.InvokeTextEmbedding(&itr, c, config.PluginMaxExecutionTimeout) + }, + ) + } +} + +func GetTextEmbeddingNumTokens(config *app.Config) gin.HandlerFunc { + type request = plugin_entities.InvokePluginRequest[requests.RequestGetTextEmbeddingNumTokens] + + return func(c *gin.Context) { + BindPluginDispatchRequest( + c, + func(itr request) { + service.GetTextEmbeddingNumTokens(&itr, c, config.PluginMaxExecutionTimeout) + }, + ) + } +} + +func InvokeRerank(config *app.Config) gin.HandlerFunc { + type request = plugin_entities.InvokePluginRequest[requests.RequestInvokeRerank] + + return func(c *gin.Context) { + BindPluginDispatchRequest( + c, + func(itr request) { + service.InvokeRerank(&itr, c, config.PluginMaxExecutionTimeout) + }, + ) + } +} + +func InvokeTTS(config *app.Config) gin.HandlerFunc { + type request = plugin_entities.InvokePluginRequest[requests.RequestInvokeTTS] + + return func(c *gin.Context) { + BindPluginDispatchRequest( + c, + func(itr request) { + service.InvokeTTS(&itr, c, config.PluginMaxExecutionTimeout) + }, + ) + } +} + +func GetTTSModelVoices(config *app.Config) gin.HandlerFunc { + type request = plugin_entities.InvokePluginRequest[requests.RequestGetTTSModelVoices] + + return func(c *gin.Context) { + BindPluginDispatchRequest( + c, + func(itr request) { + service.GetTTSModelVoices(&itr, c, config.PluginMaxExecutionTimeout) + }, + ) + } +} + +func InvokeSpeech2Text(config *app.Config) gin.HandlerFunc { + type request = plugin_entities.InvokePluginRequest[requests.RequestInvokeSpeech2Text] + + return func(c *gin.Context) { + BindPluginDispatchRequest( + c, + func(itr request) { + service.InvokeSpeech2Text(&itr, c, config.PluginMaxExecutionTimeout) + }, + ) + } +} + +func InvokeModeration(config *app.Config) gin.HandlerFunc { + type request = plugin_entities.InvokePluginRequest[requests.RequestInvokeModeration] + + return func(c *gin.Context) { + BindPluginDispatchRequest( + c, + func(itr request) { + service.InvokeModeration(&itr, c, config.PluginMaxExecutionTimeout) + }, + ) + } +} + +func ValidateProviderCredentials(config *app.Config) gin.HandlerFunc { + type request = plugin_entities.InvokePluginRequest[requests.RequestValidateProviderCredentials] + + return func(c *gin.Context) { + BindPluginDispatchRequest( + c, + func(itr request) { + service.ValidateProviderCredentials(&itr, c, config.PluginMaxExecutionTimeout) + }, + ) + } +} + +func ValidateModelCredentials(config *app.Config) gin.HandlerFunc { + type request = plugin_entities.InvokePluginRequest[requests.RequestValidateModelCredentials] + + return func(c *gin.Context) { + BindPluginDispatchRequest( + c, + func(itr request) { + service.ValidateModelCredentials(&itr, c, config.PluginMaxExecutionTimeout) + }, + ) + } +} + +func GetAIModelSchema(config *app.Config) gin.HandlerFunc { + type request = plugin_entities.InvokePluginRequest[requests.RequestGetAIModelSchema] + + return func(c *gin.Context) { + BindPluginDispatchRequest( + c, + func(itr request) { + service.GetAIModelSchema(&itr, c, config.PluginMaxExecutionTimeout) + }, + ) + } +} diff --git a/internal/server/controllers/model.go b/internal/server/controllers/model.go index d845e1f0ab..383d1ae7e3 100644 --- a/internal/server/controllers/model.go +++ b/internal/server/controllers/model.go @@ -5,167 +5,8 @@ import ( "github.com/gin-gonic/gin" "github.com/langgenius/dify-plugin-daemon/internal/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/entities/requests" ) -func InvokeLLM(config *app.Config) gin.HandlerFunc { - type request = plugin_entities.InvokePluginRequest[requests.RequestInvokeLLM] - - return func(c *gin.Context) { - BindPluginDispatchRequest( - c, - func(itr request) { - service.InvokeLLM(&itr, c, config.PluginMaxExecutionTimeout) - }, - ) - } -} - -func InvokeTextEmbedding(config *app.Config) gin.HandlerFunc { - type request = plugin_entities.InvokePluginRequest[requests.RequestInvokeTextEmbedding] - - return func(c *gin.Context) { - BindPluginDispatchRequest( - c, - func(itr request) { - service.InvokeTextEmbedding(&itr, c, config.PluginMaxExecutionTimeout) - }, - ) - } -} - -func InvokeRerank(config *app.Config) gin.HandlerFunc { - type request = plugin_entities.InvokePluginRequest[requests.RequestInvokeRerank] - - return func(c *gin.Context) { - BindPluginDispatchRequest( - c, - func(itr request) { - service.InvokeRerank(&itr, c, config.PluginMaxExecutionTimeout) - }, - ) - } -} - -func InvokeTTS(config *app.Config) gin.HandlerFunc { - type request = plugin_entities.InvokePluginRequest[requests.RequestInvokeTTS] - - return func(c *gin.Context) { - BindPluginDispatchRequest( - c, - func(itr request) { - service.InvokeTTS(&itr, c, config.PluginMaxExecutionTimeout) - }, - ) - } -} - -func InvokeSpeech2Text(config *app.Config) gin.HandlerFunc { - type request = plugin_entities.InvokePluginRequest[requests.RequestInvokeSpeech2Text] - - return func(c *gin.Context) { - BindPluginDispatchRequest( - c, - func(itr request) { - service.InvokeSpeech2Text(&itr, c, config.PluginMaxExecutionTimeout) - }, - ) - } -} - -func InvokeModeration(config *app.Config) gin.HandlerFunc { - type request = plugin_entities.InvokePluginRequest[requests.RequestInvokeModeration] - - return func(c *gin.Context) { - BindPluginDispatchRequest( - c, - func(itr request) { - service.InvokeModeration(&itr, c, config.PluginMaxExecutionTimeout) - }, - ) - } -} - -func ValidateProviderCredentials(config *app.Config) gin.HandlerFunc { - type request = plugin_entities.InvokePluginRequest[requests.RequestValidateProviderCredentials] - - return func(c *gin.Context) { - BindPluginDispatchRequest( - c, - func(itr request) { - service.ValidateProviderCredentials(&itr, c, config.PluginMaxExecutionTimeout) - }, - ) - } -} - -func ValidateModelCredentials(config *app.Config) gin.HandlerFunc { - type request = plugin_entities.InvokePluginRequest[requests.RequestValidateModelCredentials] - - return func(c *gin.Context) { - BindPluginDispatchRequest( - c, - func(itr request) { - service.ValidateModelCredentials(&itr, c, config.PluginMaxExecutionTimeout) - }, - ) - } -} - -func GetTTSModelVoices(config *app.Config) gin.HandlerFunc { - type request = plugin_entities.InvokePluginRequest[requests.RequestGetTTSModelVoices] - - return func(c *gin.Context) { - BindPluginDispatchRequest( - c, - func(itr request) { - service.GetTTSModelVoices(&itr, c, config.PluginMaxExecutionTimeout) - }, - ) - } -} - -func GetTextEmbeddingNumTokens(config *app.Config) gin.HandlerFunc { - type request = plugin_entities.InvokePluginRequest[requests.RequestGetTextEmbeddingNumTokens] - - return func(c *gin.Context) { - BindPluginDispatchRequest( - c, - func(itr request) { - service.GetTextEmbeddingNumTokens(&itr, c, config.PluginMaxExecutionTimeout) - }, - ) - } -} - -func GetLLMNumTokens(config *app.Config) gin.HandlerFunc { - type request = plugin_entities.InvokePluginRequest[requests.RequestGetLLMNumTokens] - - return func(c *gin.Context) { - BindPluginDispatchRequest( - c, - func(itr request) { - service.GetLLMNumTokens(&itr, c, config.PluginMaxExecutionTimeout) - }, - ) - } -} - -func GetAIModelSchema(config *app.Config) gin.HandlerFunc { - type request = plugin_entities.InvokePluginRequest[requests.RequestGetAIModelSchema] - - return func(c *gin.Context) { - BindPluginDispatchRequest( - c, - func(itr request) { - service.GetAIModelSchema(&itr, c, config.PluginMaxExecutionTimeout) - }, - ) - } -} - func ListModels(c *gin.Context) { BindRequest(c, func(request struct { TenantID string `uri:"tenant_id" validate:"required"` diff --git a/internal/server/controllers/oauth.gen.go b/internal/server/controllers/oauth.gen.go new file mode 100644 index 0000000000..0d69a79b81 --- /dev/null +++ b/internal/server/controllers/oauth.gen.go @@ -0,0 +1,50 @@ +// Code generated by controller generator. DO NOT EDIT. + +package controllers + +import ( + "github.com/gin-gonic/gin" + "github.com/langgenius/dify-plugin-daemon/internal/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/entities/requests" +) + +func GetAuthorizationURL(config *app.Config) gin.HandlerFunc { + type request = plugin_entities.InvokePluginRequest[requests.RequestOAuthGetAuthorizationURL] + + return func(c *gin.Context) { + BindPluginDispatchRequest( + c, + func(itr request) { + service.GetAuthorizationURL(&itr, c, config.PluginMaxExecutionTimeout) + }, + ) + } +} + +func GetCredentials(config *app.Config) gin.HandlerFunc { + type request = plugin_entities.InvokePluginRequest[requests.RequestOAuthGetCredentials] + + return func(c *gin.Context) { + BindPluginDispatchRequest( + c, + func(itr request) { + service.GetCredentials(&itr, c, config.PluginMaxExecutionTimeout) + }, + ) + } +} + +func RefreshCredentials(config *app.Config) gin.HandlerFunc { + type request = plugin_entities.InvokePluginRequest[requests.RequestOAuthRefreshCredentials] + + return func(c *gin.Context) { + BindPluginDispatchRequest( + c, + func(itr request) { + service.RefreshCredentials(&itr, c, config.PluginMaxExecutionTimeout) + }, + ) + } +} diff --git a/internal/server/controllers/plugins.go b/internal/server/controllers/plugins.go index 9f2a48b0be..e0e76ad5c6 100644 --- a/internal/server/controllers/plugins.go +++ b/internal/server/controllers/plugins.go @@ -150,6 +150,16 @@ func ReinstallPluginFromIdentifier(app *app.Config) gin.HandlerFunc { } } +func DecodePluginFromIdentifier(app *app.Config) gin.HandlerFunc { + return func(c *gin.Context) { + BindRequest(c, func(request struct { + PluginUniqueIdentifier plugin_entities.PluginUniqueIdentifier `json:"plugin_unique_identifier" validate:"required,plugin_unique_identifier"` + }) { + c.JSON(http.StatusOK, service.DecodePluginFromIdentifier(app, request.PluginUniqueIdentifier)) + }) + } +} + func FetchPluginInstallationTasks(c *gin.Context) { BindRequest(c, func(request struct { TenantID string `uri:"tenant_id" validate:"required"` diff --git a/internal/server/controllers/tool.gen.go b/internal/server/controllers/tool.gen.go new file mode 100644 index 0000000000..0b2d0322ba --- /dev/null +++ b/internal/server/controllers/tool.gen.go @@ -0,0 +1,37 @@ +// Code generated by controller generator. DO NOT EDIT. + +package controllers + +import ( + "github.com/gin-gonic/gin" + "github.com/langgenius/dify-plugin-daemon/internal/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/entities/requests" +) + +func ValidateToolCredentials(config *app.Config) gin.HandlerFunc { + type request = plugin_entities.InvokePluginRequest[requests.RequestValidateToolCredentials] + + return func(c *gin.Context) { + BindPluginDispatchRequest( + c, + func(itr request) { + service.ValidateToolCredentials(&itr, c, config.PluginMaxExecutionTimeout) + }, + ) + } +} + +func GetToolRuntimeParameters(config *app.Config) gin.HandlerFunc { + type request = plugin_entities.InvokePluginRequest[requests.RequestGetToolRuntimeParameters] + + return func(c *gin.Context) { + BindPluginDispatchRequest( + c, + func(itr request) { + service.GetToolRuntimeParameters(&itr, c, config.PluginMaxExecutionTimeout) + }, + ) + } +} diff --git a/internal/server/controllers/tool.go b/internal/server/controllers/tool.go index 3a70767c2d..096e42806a 100644 --- a/internal/server/controllers/tool.go +++ b/internal/server/controllers/tool.go @@ -23,32 +23,6 @@ func InvokeTool(config *app.Config) gin.HandlerFunc { } } -func ValidateToolCredentials(config *app.Config) gin.HandlerFunc { - type request = plugin_entities.InvokePluginRequest[requests.RequestValidateToolCredentials] - - return func(c *gin.Context) { - BindPluginDispatchRequest( - c, - func(itr request) { - service.ValidateToolCredentials(&itr, c, config.PluginMaxExecutionTimeout) - }, - ) - } -} - -func GetToolRuntimeParameters(config *app.Config) gin.HandlerFunc { - type request = plugin_entities.InvokePluginRequest[requests.RequestGetToolRuntimeParameters] - - return func(c *gin.Context) { - BindPluginDispatchRequest( - c, - func(itr request) { - service.GetToolRuntimeParameters(&itr, c, config.PluginMaxExecutionTimeout) - }, - ) - } -} - func ListTools(c *gin.Context) { BindRequest(c, func(request struct { TenantID string `uri:"tenant_id" validate:"required"` diff --git a/internal/server/http_server.gen.go b/internal/server/http_server.gen.go new file mode 100644 index 0000000000..7b42ea0215 --- /dev/null +++ b/internal/server/http_server.gen.go @@ -0,0 +1,29 @@ +// Code generated by controller generator. DO NOT EDIT. +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/langgenius/dify-plugin-daemon/internal/server/controllers" + "github.com/langgenius/dify-plugin-daemon/internal/types/app" +) + +func (app *App) setupGeneratedRoutes(group *gin.RouterGroup, config *app.Config) { + group.POST("/tool/validate_credentials", controllers.ValidateToolCredentials(config)) + group.POST("/tool/get_runtime_parameters", controllers.GetToolRuntimeParameters(config)) + group.POST("/llm/invoke", controllers.InvokeLLM(config)) + group.POST("/llm/num_tokens", controllers.GetLLMNumTokens(config)) + group.POST("/text_embedding/invoke", controllers.InvokeTextEmbedding(config)) + group.POST("/text_embedding/num_tokens", controllers.GetTextEmbeddingNumTokens(config)) + group.POST("/rerank/invoke", controllers.InvokeRerank(config)) + group.POST("/tts/invoke", controllers.InvokeTTS(config)) + group.POST("/tts/model/voices", controllers.GetTTSModelVoices(config)) + group.POST("/speech2text/invoke", controllers.InvokeSpeech2Text(config)) + group.POST("/moderation/invoke", controllers.InvokeModeration(config)) + group.POST("/model/validate_provider_credentials", controllers.ValidateProviderCredentials(config)) + group.POST("/model/validate_model_credentials", controllers.ValidateModelCredentials(config)) + group.POST("/model/schema", controllers.GetAIModelSchema(config)) + group.POST("/oauth/get_authorization_url", controllers.GetAuthorizationURL(config)) + group.POST("/oauth/get_credentials", controllers.GetCredentials(config)) + group.POST("/oauth/refresh_credentials", controllers.RefreshCredentials(config)) + group.POST("/dynamic_select/fetch_parameter_options", controllers.FetchDynamicParameterOptions(config)) +} diff --git a/internal/server/http_server.go b/internal/server/http_server.go index 9fe3782eea..da0b8c062f 100644 --- a/internal/server/http_server.go +++ b/internal/server/http_server.go @@ -27,6 +27,7 @@ func (app *App) server(config *app.Config) func() { })) } engine.Use(gin.Recovery()) + engine.Use(controllers.CollectActiveRequests()) engine.GET("/health/check", controllers.HealthCheck(config)) endpointGroup := engine.Group("/e") @@ -93,28 +94,15 @@ func (app *App) pluginGroup(group *gin.RouterGroup, config *app.Config) { } func (app *App) pluginDispatchGroup(group *gin.RouterGroup, config *app.Config) { + group.Use(controllers.CollectActiveDispatchRequests()) group.Use(app.FetchPluginInstallation()) group.Use(app.RedirectPluginInvoke()) group.Use(app.InitClusterID()) group.POST("/tool/invoke", controllers.InvokeTool(config)) - group.POST("/tool/validate_credentials", controllers.ValidateToolCredentials(config)) - group.POST("/tool/get_runtime_parameters", controllers.GetToolRuntimeParameters(config)) group.POST("/agent_strategy/invoke", controllers.InvokeAgentStrategy(config)) - group.POST("/llm/invoke", controllers.InvokeLLM(config)) - group.POST("/llm/num_tokens", controllers.GetLLMNumTokens(config)) - group.POST("/text_embedding/invoke", controllers.InvokeTextEmbedding(config)) - group.POST("/text_embedding/num_tokens", controllers.GetTextEmbeddingNumTokens(config)) - group.POST("/rerank/invoke", controllers.InvokeRerank(config)) - group.POST("/tts/invoke", controllers.InvokeTTS(config)) - group.POST("/tts/model/voices", controllers.GetTTSModelVoices(config)) - group.POST("/speech2text/invoke", controllers.InvokeSpeech2Text(config)) - group.POST("/moderation/invoke", controllers.InvokeModeration(config)) - group.POST("/model/validate_provider_credentials", controllers.ValidateProviderCredentials(config)) - group.POST("/model/validate_model_credentials", controllers.ValidateModelCredentials(config)) - group.POST("/model/schema", controllers.GetAIModelSchema(config)) - group.POST("/oauth/get_authorization_url", controllers.OAuthGetAuthorizationURL(config)) - group.POST("/oauth/get_credentials", controllers.OAuthGetCredentials(config)) + + app.setupGeneratedRoutes(group, config) } func (app *App) remoteDebuggingGroup(group *gin.RouterGroup, config *app.Config) { @@ -166,6 +154,7 @@ func (app *App) pluginManagementGroup(group *gin.RouterGroup, config *app.Config group.POST("/install/tasks/:id/delete", controllers.DeletePluginInstallationTask) group.POST("/install/tasks/:id/delete/*identifier", controllers.DeletePluginInstallationItemFromTask) group.GET("/install/tasks", controllers.FetchPluginInstallationTasks) + group.GET("/decode/from_identifier", controllers.DecodePluginFromIdentifier(config)) group.GET("/fetch/manifest", controllers.FetchPluginManifest) group.GET("/fetch/identifier", controllers.FetchPluginFromIdentifier) group.POST("/uninstall", controllers.UninstallPlugin) diff --git a/internal/server/server.go b/internal/server/server.go index f7d290c0c9..79ead81ea1 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,20 +1,13 @@ package server import ( - "context" - "github.com/getsentry/sentry-go" + "github.com/langgenius/dify-cloud-kit/oss" + "github.com/langgenius/dify-cloud-kit/oss/factory" "github.com/langgenius/dify-plugin-daemon/internal/cluster" "github.com/langgenius/dify-plugin-daemon/internal/core/persistence" "github.com/langgenius/dify-plugin-daemon/internal/core/plugin_manager" "github.com/langgenius/dify-plugin-daemon/internal/db" - "github.com/langgenius/dify-plugin-daemon/internal/oss" - "github.com/langgenius/dify-plugin-daemon/internal/oss/aliyun" - "github.com/langgenius/dify-plugin-daemon/internal/oss/azure" - "github.com/langgenius/dify-plugin-daemon/internal/oss/gcs" - "github.com/langgenius/dify-plugin-daemon/internal/oss/local" - "github.com/langgenius/dify-plugin-daemon/internal/oss/s3" - "github.com/langgenius/dify-plugin-daemon/internal/oss/tencent_cos" "github.com/langgenius/dify-plugin-daemon/internal/types/app" "github.com/langgenius/dify-plugin-daemon/internal/utils/log" "github.com/langgenius/dify-plugin-daemon/internal/utils/routine" @@ -22,50 +15,59 @@ import ( func initOSS(config *app.Config) oss.OSS { // init storage - ctx := context.TODO() var storage oss.OSS var err error - switch config.PluginStorageType { - case oss.OSS_TYPE_S3: - storage, err = s3.NewS3Storage( - config.S3UseAwsManagedIam, - config.S3Endpoint, - config.S3UsePathStyle, - config.AWSAccessKey, - config.AWSSecretKey, - config.PluginStorageOSSBucket, - config.AWSRegion, - ) - case oss.OSS_TYPE_LOCAL: - storage = local.NewLocalStorage(config.PluginStorageLocalRoot) - case oss.OSS_TYPE_TENCENT_COS: - storage, err = tencent_cos.NewTencentCOSStorage( - config.TencentCOSSecretId, - config.TencentCOSSecretKey, - config.TencentCOSRegion, - config.PluginStorageOSSBucket, - ) - case oss.OSS_TYPE_AZURE_BLOB: - storage, err = azure.NewAzureBlobStorage( - config.AzureBlobStorageContainerName, - config.AzureBlobStorageConnectionString, - ) - case oss.OSS_TYPE_GCS: - storage, err = gcs.NewGCSStorage(ctx, config.PluginStorageOSSBucket) - case oss.OSS_TYPE_ALIYUN_OSS: - storage, err = aliyun.NewAliyunOSSStorage( - config.AliyunOSSRegion, - config.AliyunOSSEndpoint, - config.AliyunOSSAccessKeyID, - config.AliyunOSSAccessKeySecret, - config.AliyunOSSAuthVersion, - config.AliyunOSSPath, - config.PluginStorageOSSBucket, - ) - default: - log.Panic("Invalid plugin storage type: %s", config.PluginStorageType) - } - + storage, err = factory.Load(config.PluginStorageType, oss.OSSArgs{ + Local: &oss.Local{ + Path: config.PluginStorageLocalRoot, + }, + S3: &oss.S3{ + UseAws: config.S3UseAWS, + Endpoint: config.S3Endpoint, + UsePathStyle: config.S3UsePathStyle, + AccessKey: config.AWSAccessKey, + SecretKey: config.AWSSecretKey, + Bucket: config.PluginStorageOSSBucket, + Region: config.AWSRegion, + UseIamRole: config.S3UseAwsManagedIam, + }, + TencentCOS: &oss.TencentCOS{ + Region: config.TencentCOSRegion, + SecretID: config.TencentCOSSecretId, + SecretKey: config.TencentCOSSecretKey, + Bucket: config.PluginStorageOSSBucket, + }, + AzureBlob: &oss.AzureBlob{ + ConnectionString: config.AzureBlobStorageConnectionString, + ContainerName: config.AzureBlobStorageContainerName, + }, + GoogleCloudStorage: &oss.GoogleCloudStorage{ + Bucket: config.PluginStorageOSSBucket, + CredentialsB64: config.GoogleCloudStorageCredentialsB64, + }, + AliyunOSS: &oss.AliyunOSS{ + Region: config.AliyunOSSRegion, + Endpoint: config.AliyunOSSEndpoint, + AccessKey: config.AliyunOSSAccessKeyID, + SecretKey: config.AliyunOSSAccessKeySecret, + AuthVersion: config.AliyunOSSAuthVersion, + Path: config.AliyunOSSPath, + Bucket: config.PluginStorageOSSBucket, + }, + HuaweiOBS: &oss.HuaweiOBS{ + AccessKey: config.HuaweiOBSAccessKey, + SecretKey: config.HuaweiOBSSecretKey, + Server: config.HuaweiOBSServer, + Bucket: config.PluginStorageOSSBucket, + }, + VolcengineTOS: &oss.VolcengineTOS{ + Region: config.VolcengineTOSRegion, + Endpoint: config.VolcengineTOSEndpoint, + AccessKey: config.VolcengineTOSAccessKey, + SecretKey: config.VolcengineTOSSecretKey, + Bucket: config.PluginStorageOSSBucket, + }, + }) if err != nil { log.Panic("Failed to create storage: %s", err) } diff --git a/internal/service/dynamic_select.gen.go b/internal/service/dynamic_select.gen.go new file mode 100644 index 0000000000..bf3840e836 --- /dev/null +++ b/internal/service/dynamic_select.gen.go @@ -0,0 +1,31 @@ +// Code generated by controller generator. DO NOT EDIT. + +package service + +import ( + "github.com/gin-gonic/gin" + "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/session_manager" + "github.com/langgenius/dify-plugin-daemon/internal/utils/stream" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/dynamic_select_entities" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/requests" +) + +func FetchDynamicParameterOptions( + r *plugin_entities.InvokePluginRequest[requests.RequestDynamicParameterSelect], + ctx *gin.Context, + max_timeout_seconds int, +) { + baseSSEWithSession( + func(session *session_manager.Session) (*stream.Stream[dynamic_select_entities.DynamicSelectResult], error) { + return plugin_daemon.FetchDynamicParameterOptions(session, &r.Data) + }, + access_types.PLUGIN_ACCESS_TYPE_DYNAMIC_PARAMETER, + access_types.PLUGIN_ACCESS_ACTION_DYNAMIC_PARAMETER_FETCH_OPTIONS, + r, + ctx, + max_timeout_seconds, + ) +} diff --git a/internal/service/install_plugin.go b/internal/service/install_plugin.go index 3977b0f273..0b5861f1cf 100644 --- a/internal/service/install_plugin.go +++ b/internal/service/install_plugin.go @@ -81,6 +81,7 @@ func InstallPluginRuntimeToTenant( PluginID: pluginUniqueIdentifier.PluginID(), Status: models.InstallTaskStatusPending, Icon: pluginDeclaration.Icon, + IconDark: pluginDeclaration.IconDark, Labels: pluginDeclaration.Label, Message: "", }) @@ -368,6 +369,50 @@ func ReinstallPluginFromIdentifier( }, ctx, 1800) } +/* + * Decode a plugin from a given identifier, no tenant_id is needed + * When upload local plugin inside Dify, the second step need to ensure that the plugin is valid + * So we need to provide a way to decode the plugin and verify the signature + */ +func DecodePluginFromIdentifier( + config *app.Config, + pluginUniqueIdentifier plugin_entities.PluginUniqueIdentifier, +) *entities.Response { + // get plugin package and decode again + manager := plugin_manager.Manager() + pkgFile, err := manager.GetPackage(pluginUniqueIdentifier) + if err != nil { + return exception.BadRequestError(err).ToResponse() + } + + zipDecoder, err := decoder.NewZipPluginDecoderWithThirdPartySignatureVerificationConfig( + pkgFile, + &decoder.ThirdPartySignatureVerificationConfig{ + Enabled: config.ThirdPartySignatureVerificationEnabled, + PublicKeyPaths: config.ThirdPartySignatureVerificationPublicKeys, + }, + ) + if err != nil { + return exception.BadRequestError(err).ToResponse() + } + + verification, _ := zipDecoder.Verification() + if verification == nil && zipDecoder.Verified() { + verification = decoder.DefaultVerification() + } + + declaration, err := zipDecoder.Manifest() + if err != nil { + return exception.BadRequestError(err).ToResponse() + } + + return entities.NewSuccessResponse(map[string]any{ + "unique_identifier": pluginUniqueIdentifier, + "manifest": declaration, + "verification": verification, + }) +} + func UpgradePlugin( config *app.Config, tenant_id string, diff --git a/internal/service/invoke_tool.go b/internal/service/invoke_tool.go index 9d2422d37b..baadd4b592 100644 --- a/internal/service/invoke_tool.go +++ b/internal/service/invoke_tool.go @@ -27,37 +27,3 @@ func InvokeTool( max_timeout_seconds, ) } - -func ValidateToolCredentials( - r *plugin_entities.InvokePluginRequest[requests.RequestValidateToolCredentials], - ctx *gin.Context, - max_timeout_seconds int, -) { - baseSSEWithSession( - func(session *session_manager.Session) (*stream.Stream[tool_entities.ValidateCredentialsResult], error) { - return plugin_daemon.ValidateToolCredentials(session, &r.Data) - }, - access_types.PLUGIN_ACCESS_TYPE_TOOL, - access_types.PLUGIN_ACCESS_ACTION_VALIDATE_TOOL_CREDENTIALS, - r, - ctx, - max_timeout_seconds, - ) -} - -func GetToolRuntimeParameters( - r *plugin_entities.InvokePluginRequest[requests.RequestGetToolRuntimeParameters], - ctx *gin.Context, - max_timeout_seconds int, -) { - baseSSEWithSession( - func(session *session_manager.Session) (*stream.Stream[tool_entities.GetToolRuntimeParametersResponse], error) { - return plugin_daemon.GetToolRuntimeParameters(session, &r.Data) - }, - access_types.PLUGIN_ACCESS_TYPE_TOOL, - access_types.PLUGIN_ACCESS_ACTION_GET_TOOL_RUNTIME_PARAMETERS, - r, - ctx, - max_timeout_seconds, - ) -} diff --git a/internal/service/manage_plugin.go b/internal/service/manage_plugin.go index 95bf9d6ce9..b9794a020a 100644 --- a/internal/service/manage_plugin.go +++ b/internal/service/manage_plugin.go @@ -34,8 +34,23 @@ func ListPlugins(tenant_id string, page int, page_size int) *entities.Response { Meta map[string]any `json:"meta"` } + type responseData struct { + List []installation `json:"list"` + Total int64 `json:"total"` + } + + // get total count + totalCount, err := db.GetCount[models.PluginInstallation]( + db.Equal("tenant_id", tenant_id), + ) + + if err != nil { + return exception.InternalServerError(err).ToResponse() + } + pluginInstallations, err := db.GetAll[models.PluginInstallation]( db.Equal("tenant_id", tenant_id), + db.OrderBy("created_at", true), db.Page(page, page_size), ) @@ -81,7 +96,12 @@ func ListPlugins(tenant_id string, page int, page_size int) *entities.Response { }) } - return entities.NewSuccessResponse(data) + finalData := responseData{ + List: data, + Total: totalCount, + } + + return entities.NewSuccessResponse(finalData) } // Using plugin_ids to fetch plugin installations @@ -375,6 +395,7 @@ func ListAgentStrategies(tenant_id string, page int, page_size int) *entities.Re models.AgentStrategyInstallation // pointer to avoid deep copy Declaration *plugin_entities.AgentStrategyProviderDeclaration `json:"declaration"` + Meta plugin_entities.PluginMeta `json:"meta"` } providers, err := db.GetAll[models.AgentStrategyInstallation]( @@ -409,6 +430,7 @@ func ListAgentStrategies(tenant_id string, page int, page_size int) *entities.Re data = append(data, AgentStrategy{ AgentStrategyInstallation: provider, Declaration: declaration.AgentStrategy, + Meta: declaration.Meta, }) } @@ -420,6 +442,7 @@ func GetAgentStrategy(tenant_id string, plugin_id string, provider string) *enti models.AgentStrategyInstallation // pointer to avoid deep copy Declaration *plugin_entities.AgentStrategyProviderDeclaration `json:"declaration"` + Meta plugin_entities.PluginMeta `json:"meta"` } agent_strategy, err := db.GetOne[models.AgentStrategyInstallation]( @@ -459,5 +482,6 @@ func GetAgentStrategy(tenant_id string, plugin_id string, provider string) *enti return entities.NewSuccessResponse(AgentStrategy{ AgentStrategyInstallation: agent_strategy, Declaration: declaration.AgentStrategy, + Meta: declaration.Meta, }) } diff --git a/internal/service/invoke_model.go b/internal/service/model.gen.go similarity index 99% rename from internal/service/invoke_model.go rename to internal/service/model.gen.go index f68e5766e4..af45f8e339 100644 --- a/internal/service/invoke_model.go +++ b/internal/service/model.gen.go @@ -1,3 +1,5 @@ +// Code generated by controller generator. DO NOT EDIT. + package service import ( @@ -28,6 +30,23 @@ func InvokeLLM( ) } +func GetLLMNumTokens( + r *plugin_entities.InvokePluginRequest[requests.RequestGetLLMNumTokens], + ctx *gin.Context, + max_timeout_seconds int, +) { + baseSSEWithSession( + func(session *session_manager.Session) (*stream.Stream[model_entities.LLMGetNumTokensResponse], error) { + return plugin_daemon.GetLLMNumTokens(session, &r.Data) + }, + access_types.PLUGIN_ACCESS_TYPE_MODEL, + access_types.PLUGIN_ACCESS_ACTION_GET_LLM_NUM_TOKENS, + r, + ctx, + max_timeout_seconds, + ) +} + func InvokeTextEmbedding( r *plugin_entities.InvokePluginRequest[requests.RequestInvokeTextEmbedding], ctx *gin.Context, @@ -45,6 +64,23 @@ func InvokeTextEmbedding( ) } +func GetTextEmbeddingNumTokens( + r *plugin_entities.InvokePluginRequest[requests.RequestGetTextEmbeddingNumTokens], + ctx *gin.Context, + max_timeout_seconds int, +) { + baseSSEWithSession( + func(session *session_manager.Session) (*stream.Stream[model_entities.GetTextEmbeddingNumTokensResponse], error) { + return plugin_daemon.GetTextEmbeddingNumTokens(session, &r.Data) + }, + access_types.PLUGIN_ACCESS_TYPE_MODEL, + access_types.PLUGIN_ACCESS_ACTION_GET_TEXT_EMBEDDING_NUM_TOKENS, + r, + ctx, + max_timeout_seconds, + ) +} + func InvokeRerank( r *plugin_entities.InvokePluginRequest[requests.RequestInvokeRerank], ctx *gin.Context, @@ -79,6 +115,23 @@ func InvokeTTS( ) } +func GetTTSModelVoices( + r *plugin_entities.InvokePluginRequest[requests.RequestGetTTSModelVoices], + ctx *gin.Context, + max_timeout_seconds int, +) { + baseSSEWithSession( + func(session *session_manager.Session) (*stream.Stream[model_entities.GetTTSVoicesResponse], error) { + return plugin_daemon.GetTTSModelVoices(session, &r.Data) + }, + access_types.PLUGIN_ACCESS_TYPE_MODEL, + access_types.PLUGIN_ACCESS_ACTION_GET_TTS_MODEL_VOICES, + r, + ctx, + max_timeout_seconds, + ) +} + func InvokeSpeech2Text( r *plugin_entities.InvokePluginRequest[requests.RequestInvokeSpeech2Text], ctx *gin.Context, @@ -147,40 +200,6 @@ func ValidateModelCredentials( ) } -func GetTTSModelVoices( - r *plugin_entities.InvokePluginRequest[requests.RequestGetTTSModelVoices], - ctx *gin.Context, - max_timeout_seconds int, -) { - baseSSEWithSession( - func(session *session_manager.Session) (*stream.Stream[model_entities.GetTTSVoicesResponse], error) { - return plugin_daemon.GetTTSModelVoices(session, &r.Data) - }, - access_types.PLUGIN_ACCESS_TYPE_MODEL, - access_types.PLUGIN_ACCESS_ACTION_GET_TTS_MODEL_VOICES, - r, - ctx, - max_timeout_seconds, - ) -} - -func GetTextEmbeddingNumTokens( - r *plugin_entities.InvokePluginRequest[requests.RequestGetTextEmbeddingNumTokens], - ctx *gin.Context, - max_timeout_seconds int, -) { - baseSSEWithSession( - func(session *session_manager.Session) (*stream.Stream[model_entities.GetTextEmbeddingNumTokensResponse], error) { - return plugin_daemon.GetTextEmbeddingNumTokens(session, &r.Data) - }, - access_types.PLUGIN_ACCESS_TYPE_MODEL, - access_types.PLUGIN_ACCESS_ACTION_GET_TEXT_EMBEDDING_NUM_TOKENS, - r, - ctx, - max_timeout_seconds, - ) -} - func GetAIModelSchema( r *plugin_entities.InvokePluginRequest[requests.RequestGetAIModelSchema], ctx *gin.Context, @@ -197,20 +216,3 @@ func GetAIModelSchema( max_timeout_seconds, ) } - -func GetLLMNumTokens( - r *plugin_entities.InvokePluginRequest[requests.RequestGetLLMNumTokens], - ctx *gin.Context, - max_timeout_seconds int, -) { - baseSSEWithSession( - func(session *session_manager.Session) (*stream.Stream[model_entities.LLMGetNumTokensResponse], error) { - return plugin_daemon.GetLLMNumTokens(session, &r.Data) - }, - access_types.PLUGIN_ACCESS_TYPE_MODEL, - access_types.PLUGIN_ACCESS_ACTION_GET_LLM_NUM_TOKENS, - r, - ctx, - max_timeout_seconds, - ) -} diff --git a/internal/service/oauth.gen.go b/internal/service/oauth.gen.go new file mode 100644 index 0000000000..ff83c21fb7 --- /dev/null +++ b/internal/service/oauth.gen.go @@ -0,0 +1,65 @@ +// Code generated by controller generator. DO NOT EDIT. + +package service + +import ( + "github.com/gin-gonic/gin" + "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/session_manager" + "github.com/langgenius/dify-plugin-daemon/internal/utils/stream" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/oauth_entities" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/requests" +) + +func GetAuthorizationURL( + r *plugin_entities.InvokePluginRequest[requests.RequestOAuthGetAuthorizationURL], + ctx *gin.Context, + max_timeout_seconds int, +) { + baseSSEWithSession( + func(session *session_manager.Session) (*stream.Stream[oauth_entities.OAuthGetAuthorizationURLResult], error) { + return plugin_daemon.GetAuthorizationURL(session, &r.Data) + }, + access_types.PLUGIN_ACCESS_TYPE_OAUTH, + access_types.PLUGIN_ACCESS_ACTION_GET_AUTHORIZATION_URL, + r, + ctx, + max_timeout_seconds, + ) +} + +func GetCredentials( + r *plugin_entities.InvokePluginRequest[requests.RequestOAuthGetCredentials], + ctx *gin.Context, + max_timeout_seconds int, +) { + baseSSEWithSession( + func(session *session_manager.Session) (*stream.Stream[oauth_entities.OAuthGetCredentialsResult], error) { + return plugin_daemon.GetCredentials(session, &r.Data) + }, + access_types.PLUGIN_ACCESS_TYPE_OAUTH, + access_types.PLUGIN_ACCESS_ACTION_GET_CREDENTIALS, + r, + ctx, + max_timeout_seconds, + ) +} + +func RefreshCredentials( + r *plugin_entities.InvokePluginRequest[requests.RequestOAuthRefreshCredentials], + ctx *gin.Context, + max_timeout_seconds int, +) { + baseSSEWithSession( + func(session *session_manager.Session) (*stream.Stream[oauth_entities.OAuthRefreshCredentialsResult], error) { + return plugin_daemon.RefreshCredentials(session, &r.Data) + }, + access_types.PLUGIN_ACCESS_TYPE_OAUTH, + access_types.PLUGIN_ACCESS_ACTION_REFRESH_CREDENTIALS, + r, + ctx, + max_timeout_seconds, + ) +} diff --git a/internal/service/plugin_decoder.go b/internal/service/plugin_decoder.go index 7218ebb030..31dfe64144 100644 --- a/internal/service/plugin_decoder.go +++ b/internal/service/plugin_decoder.go @@ -20,11 +20,11 @@ import ( func UploadPluginPkg( config *app.Config, c *gin.Context, - tenant_id string, - dify_pkg_file multipart.File, - verify_signature bool, + tenantId string, + difyPkgFile multipart.File, + verifySignature bool, ) *entities.Response { - pluginFile, err := io.ReadAll(dify_pkg_file) + pluginFile, err := io.ReadAll(difyPkgFile) if err != nil { return exception.InternalServerError(err).ToResponse() } @@ -53,7 +53,7 @@ func UploadPluginPkg( return exception.BadRequestError(errors.Join(err, errors.New("failed to save package"))).ToResponse() } - if config.ForceVerifyingSignature != nil && *config.ForceVerifyingSignature || verify_signature { + if config.ForceVerifyingSignature != nil && *config.ForceVerifyingSignature || verifySignature { if !declaration.Verified { return exception.BadRequestError(errors.Join(err, errors.New( "plugin verification has been enabled, and the plugin you want to install has a bad signature", @@ -61,9 +61,15 @@ func UploadPluginPkg( } } + verification, _ := decoderInstance.Verification() + if verification == nil && decoderInstance.Verified() { + verification = decoder.DefaultVerification() + } + return entities.NewSuccessResponse(map[string]any{ "unique_identifier": pluginUniqueIdentifier, "manifest": declaration, + "verification": verification, }) } diff --git a/internal/service/session.go b/internal/service/session.go index 95e173d336..7662362ffb 100644 --- a/internal/service/session.go +++ b/internal/service/session.go @@ -42,6 +42,7 @@ func createSession[T any]( MessageID: r.MessageID, AppID: r.AppID, EndpointID: r.EndpointID, + Context: r.Context, }, ) diff --git a/internal/service/tool.gen.go b/internal/service/tool.gen.go new file mode 100644 index 0000000000..425f1e6f7a --- /dev/null +++ b/internal/service/tool.gen.go @@ -0,0 +1,48 @@ +// Code generated by controller generator. DO NOT EDIT. + +package service + +import ( + "github.com/gin-gonic/gin" + "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/session_manager" + "github.com/langgenius/dify-plugin-daemon/internal/utils/stream" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/requests" + "github.com/langgenius/dify-plugin-daemon/pkg/entities/tool_entities" +) + +func ValidateToolCredentials( + r *plugin_entities.InvokePluginRequest[requests.RequestValidateToolCredentials], + ctx *gin.Context, + max_timeout_seconds int, +) { + baseSSEWithSession( + func(session *session_manager.Session) (*stream.Stream[tool_entities.ValidateCredentialsResult], error) { + return plugin_daemon.ValidateToolCredentials(session, &r.Data) + }, + access_types.PLUGIN_ACCESS_TYPE_TOOL, + access_types.PLUGIN_ACCESS_ACTION_VALIDATE_TOOL_CREDENTIALS, + r, + ctx, + max_timeout_seconds, + ) +} + +func GetToolRuntimeParameters( + r *plugin_entities.InvokePluginRequest[requests.RequestGetToolRuntimeParameters], + ctx *gin.Context, + max_timeout_seconds int, +) { + baseSSEWithSession( + func(session *session_manager.Session) (*stream.Stream[tool_entities.GetToolRuntimeParametersResponse], error) { + return plugin_daemon.GetToolRuntimeParameters(session, &r.Data) + }, + access_types.PLUGIN_ACCESS_TYPE_TOOL, + access_types.PLUGIN_ACCESS_ACTION_GET_TOOL_RUNTIME_PARAMETERS, + r, + ctx, + max_timeout_seconds, + ) +} diff --git a/internal/types/app/config.go b/internal/types/app/config.go index e66d66354f..06f65f1d56 100644 --- a/internal/types/app/config.go +++ b/internal/types/app/config.go @@ -3,8 +3,6 @@ package app import ( "fmt" - "github.com/langgenius/dify-plugin-daemon/internal/oss" - "github.com/go-playground/validator/v10" ) @@ -21,20 +19,30 @@ type Config struct { DifyInnerApiURL string `envconfig:"DIFY_INNER_API_URL" validate:"required"` DifyInnerApiKey string `envconfig:"DIFY_INNER_API_KEY" validate:"required"` - S3UseAwsManagedIam bool `envconfig:"S3_USE_AWS_MANAGED_IAM" default:"true"` + // storage config + // https://github.com/langgenius/dify-cloud-kit/blob/main/oss/factory/factory.go + PluginStorageType string `envconfig:"PLUGIN_STORAGE_TYPE" validate:"required"` + PluginStorageOSSBucket string `envconfig:"PLUGIN_STORAGE_OSS_BUCKET"` + + // aws s3 + S3UseAwsManagedIam bool `envconfig:"S3_USE_AWS_MANAGED_IAM" default:"false"` + S3UseAWS bool `envconfig:"S3_USE_AWS" default:"true"` S3Endpoint string `envconfig:"S3_ENDPOINT"` S3UsePathStyle bool `envconfig:"S3_USE_PATH_STYLE" default:"true"` AWSAccessKey string `envconfig:"AWS_ACCESS_KEY"` AWSSecretKey string `envconfig:"AWS_SECRET_KEY"` AWSRegion string `envconfig:"AWS_REGION"` + // tencent cos TencentCOSSecretKey string `envconfig:"TENCENT_COS_SECRET_KEY"` TencentCOSSecretId string `envconfig:"TENCENT_COS_SECRET_ID"` TencentCOSRegion string `envconfig:"TENCENT_COS_REGION"` + // azure blob AzureBlobStorageContainerName string `envconfig:"AZURE_BLOB_STORAGE_CONTAINER_NAME"` AzureBlobStorageConnectionString string `envconfig:"AZURE_BLOB_STORAGE_CONNECTION_STRING"` + // aliyun oss AliyunOSSRegion string `envconfig:"ALIYUN_OSS_REGION"` AliyunOSSEndpoint string `envconfig:"ALIYUN_OSS_ENDPOINT"` AliyunOSSAccessKeyID string `envconfig:"ALIYUN_OSS_ACCESS_KEY_ID"` @@ -42,8 +50,21 @@ type Config struct { AliyunOSSAuthVersion string `envconfig:"ALIYUN_OSS_AUTH_VERSION" default:"v4"` AliyunOSSPath string `envconfig:"ALIYUN_OSS_PATH"` - PluginStorageType string `envconfig:"PLUGIN_STORAGE_TYPE" validate:"required,oneof=local aws_s3 tencent_cos azure_blob gcs aliyun_oss"` - PluginStorageOSSBucket string `envconfig:"PLUGIN_STORAGE_OSS_BUCKET"` + // google gcs + GoogleCloudStorageCredentialsB64 string `envconfig:"GCS_CREDENTIALS"` + + // huawei obs + HuaweiOBSAccessKey string `envconfig:"HUAWEI_OBS_ACCESS_KEY"` + HuaweiOBSSecretKey string `envconfig:"HUAWEI_OBS_SECRET_KEY"` + HuaweiOBSServer string `envconfig:"HUAWEI_OBS_SERVER"` + + // volcengine tos + VolcengineTOSEndpoint string `envconfig:"VOLCENGINE_TOS_ENDPOINT"` + VolcengineTOSAccessKey string `envconfig:"VOLCENGINE_TOS_ACCESS_KEY"` + VolcengineTOSSecretKey string `envconfig:"VOLCENGINE_TOS_SECRET_KEY"` + VolcengineTOSRegion string `envconfig:"VOLCENGINE_TOS_REGION"` + + // local PluginStorageLocalRoot string `envconfig:"PLUGIN_STORAGE_LOCAL_ROOT"` // plugin remote installing @@ -86,6 +107,14 @@ type Config struct { RedisUseSsl bool `envconfig:"REDIS_USE_SSL"` RedisDB int `envconfig:"REDIS_DB"` + // redis sentinel + RedisUseSentinel bool `envconfig:"REDIS_USE_SENTINEL"` + RedisSentinels string `envconfig:"REDIS_SENTINELS"` + RedisSentinelServiceName string `envconfig:"REDIS_SENTINEL_SERVICE_NAME"` + RedisSentinelUsername string `envconfig:"REDIS_SENTINEL_USERNAME"` + RedisSentinelPassword string `envconfig:"REDIS_SENTINEL_PASSWORD"` + RedisSentinelSocketTimeout float64 `envconfig:"REDIS_SENTINEL_SOCKET_TIMEOUT"` + // database DBType string `envconfig:"DB_TYPE" default:"postgresql"` DBUsername string `envconfig:"DB_USERNAME" validate:"required"` @@ -97,9 +126,11 @@ type Config struct { DBSslMode string `envconfig:"DB_SSL_MODE" validate:"required,oneof=disable require"` // database connection pool settings - DBMaxIdleConns int `envconfig:"DB_MAX_IDLE_CONNS" default:"10"` - DBMaxOpenConns int `envconfig:"DB_MAX_OPEN_CONNS" default:"30"` - DBConnMaxLifetime int `envconfig:"DB_CONN_MAX_LIFETIME" default:"3600"` + DBMaxIdleConns int `envconfig:"DB_MAX_IDLE_CONNS" default:"10"` + DBMaxOpenConns int `envconfig:"DB_MAX_OPEN_CONNS" default:"30"` + DBConnMaxLifetime int `envconfig:"DB_CONN_MAX_LIFETIME" default:"3600"` + DBExtras string `envconfig:"DB_EXTRAS"` + DBCharset string `envconfig:"DB_CHARSET"` // persistence storage PersistenceStoragePath string `envconfig:"PERSISTENCE_STORAGE_PATH"` @@ -211,44 +242,6 @@ func (c *Config) Validate() error { return fmt.Errorf("plugin package cache path is empty") } - if c.PluginStorageType == oss.OSS_TYPE_S3 { - if c.PluginStorageOSSBucket == "" { - return fmt.Errorf("plugin storage bucket is empty") - } - - if c.AWSRegion == "" { - return fmt.Errorf("aws region is empty") - } - } - - if c.PluginStorageType == oss.OSS_TYPE_AZURE_BLOB { - if c.AzureBlobStorageConnectionString == "" { - return fmt.Errorf("azure blob storage connection string is empty") - } - - if c.AzureBlobStorageContainerName == "" { - return fmt.Errorf("azure blob storage container name is empty") - } - } - - if c.PluginStorageType == oss.OSS_TYPE_ALIYUN_OSS { - if c.PluginStorageOSSBucket == "" { - return fmt.Errorf("plugin storage bucket is empty") - } - - if c.AliyunOSSEndpoint == "" { - return fmt.Errorf("aliyun oss endpoint is empty") - } - - if c.AliyunOSSAccessKeyID == "" { - return fmt.Errorf("aliyun oss access key id is empty") - } - - if c.AliyunOSSAccessKeySecret == "" { - return fmt.Errorf("aliyun oss access key secret is empty") - } - } - return nil } diff --git a/internal/types/app/default.go b/internal/types/app/default.go index 4fcd2fbd23..1c647167ab 100644 --- a/internal/types/app/default.go +++ b/internal/types/app/default.go @@ -1,7 +1,7 @@ package app import ( - "github.com/langgenius/dify-plugin-daemon/internal/oss" + "github.com/langgenius/dify-cloud-kit/oss" "golang.org/x/exp/constraints" ) diff --git a/internal/types/models/curd/atomic.go b/internal/types/models/curd/atomic.go index 150d865e03..8b1a9acb00 100644 --- a/internal/types/models/curd/atomic.go +++ b/internal/types/models/curd/atomic.go @@ -17,9 +17,9 @@ import ( // and install it to the tenant, return the plugin and the installation // if the plugin has been created before, return the plugin which has been created before func InstallPlugin( - tenant_id string, - plugin_unique_identifier plugin_entities.PluginUniqueIdentifier, - install_type plugin_entities.PluginRuntimeType, + tenantId string, + pluginUniqueIdentifier plugin_entities.PluginUniqueIdentifier, + installType plugin_entities.PluginRuntimeType, declaration *plugin_entities.PluginDeclaration, source string, meta map[string]any, @@ -32,8 +32,8 @@ func InstallPlugin( // check if already installed _, err := db.GetOne[models.PluginInstallation]( - db.Equal("plugin_id", plugin_unique_identifier.PluginID()), - db.Equal("tenant_id", tenant_id), + db.Equal("plugin_id", pluginUniqueIdentifier.PluginID()), + db.Equal("tenant_id", tenantId), ) if err == nil { @@ -43,21 +43,21 @@ func InstallPlugin( err = db.WithTransaction(func(tx *gorm.DB) error { p, err := db.GetOne[models.Plugin]( db.WithTransactionContext(tx), - db.Equal("plugin_unique_identifier", plugin_unique_identifier.String()), - db.Equal("plugin_id", plugin_unique_identifier.PluginID()), - db.Equal("install_type", string(install_type)), + db.Equal("plugin_unique_identifier", pluginUniqueIdentifier.String()), + db.Equal("plugin_id", pluginUniqueIdentifier.PluginID()), + db.Equal("install_type", string(installType)), db.WLock(), ) if err == db.ErrDatabaseNotFound { plugin := &models.Plugin{ - PluginID: plugin_unique_identifier.PluginID(), - PluginUniqueIdentifier: plugin_unique_identifier.String(), - InstallType: install_type, + PluginID: pluginUniqueIdentifier.PluginID(), + PluginUniqueIdentifier: pluginUniqueIdentifier.String(), + InstallType: installType, Refers: 1, } - if install_type == plugin_entities.PLUGIN_RUNTIME_TYPE_REMOTE { + if installType == plugin_entities.PLUGIN_RUNTIME_TYPE_REMOTE { plugin.RemoteDeclaration = *declaration } @@ -82,8 +82,8 @@ func InstallPlugin( if err := db.DeleteByCondition( models.PluginInstallation{ PluginID: pluginToBeReturns.PluginID, - RuntimeType: string(install_type), - TenantID: tenant_id, + RuntimeType: string(installType), + TenantID: tenantId, }, tx, ); err != nil { @@ -93,8 +93,8 @@ func InstallPlugin( installation := &models.PluginInstallation{ PluginID: pluginToBeReturns.PluginID, PluginUniqueIdentifier: pluginToBeReturns.PluginUniqueIdentifier, - TenantID: tenant_id, - RuntimeType: string(install_type), + TenantID: tenantId, + RuntimeType: string(installType), Source: source, Meta: meta, } @@ -111,7 +111,7 @@ func InstallPlugin( toolInstallation := &models.ToolInstallation{ PluginID: pluginToBeReturns.PluginID, PluginUniqueIdentifier: pluginToBeReturns.PluginUniqueIdentifier, - TenantID: tenant_id, + TenantID: tenantId, Provider: declaration.Tool.Identity.Name, } @@ -126,7 +126,7 @@ func InstallPlugin( agentStrategyInstallation := &models.AgentStrategyInstallation{ PluginID: pluginToBeReturns.PluginID, PluginUniqueIdentifier: pluginToBeReturns.PluginUniqueIdentifier, - TenantID: tenant_id, + TenantID: tenantId, Provider: declaration.AgentStrategy.Identity.Name, } @@ -141,7 +141,7 @@ func InstallPlugin( modelInstallation := &models.AIModelInstallation{ PluginID: pluginToBeReturns.PluginID, PluginUniqueIdentifier: pluginToBeReturns.PluginUniqueIdentifier, - TenantID: tenant_id, + TenantID: tenantId, Provider: declaration.Model.Provider, } @@ -174,26 +174,26 @@ type DeletePluginResponse struct { // and uninstall it from the tenant, return the plugin and the installation // if the plugin has been created before, return the plugin which has been created before func UninstallPlugin( - tenant_id string, - plugin_unique_identifier plugin_entities.PluginUniqueIdentifier, - installation_id string, + tenantId string, + pluginUniqueIdentifier plugin_entities.PluginUniqueIdentifier, + installationId string, declaration *plugin_entities.PluginDeclaration, ) (*DeletePluginResponse, error) { var pluginToBeReturns *models.Plugin var installationToBeReturns *models.PluginInstallation _, err := db.GetOne[models.PluginInstallation]( - db.Equal("id", installation_id), - db.Equal("plugin_unique_identifier", plugin_unique_identifier.String()), - db.Equal("tenant_id", tenant_id), + db.Equal("id", installationId), + db.Equal("plugin_unique_identifier", pluginUniqueIdentifier.String()), + db.Equal("tenant_id", tenantId), ) pluginInstallationCacheKey := strings.Join( []string{ "plugin_id", - plugin_unique_identifier.PluginID(), + pluginUniqueIdentifier.PluginID(), "tenant_id", - tenant_id, + tenantId, }, ":", ) @@ -211,7 +211,7 @@ func UninstallPlugin( err = db.WithTransaction(func(tx *gorm.DB) error { p, err := db.GetOne[models.Plugin]( db.WithTransactionContext(tx), - db.Equal("plugin_unique_identifier", plugin_unique_identifier.String()), + db.Equal("plugin_unique_identifier", pluginUniqueIdentifier.String()), db.WLock(), ) @@ -230,8 +230,8 @@ func UninstallPlugin( installation, err := db.GetOne[models.PluginInstallation]( db.WithTransactionContext(tx), - db.Equal("plugin_unique_identifier", plugin_unique_identifier.String()), - db.Equal("tenant_id", tenant_id), + db.Equal("plugin_unique_identifier", pluginUniqueIdentifier.String()), + db.Equal("tenant_id", tenantId), ) if err == db.ErrDatabaseNotFound { @@ -250,7 +250,7 @@ func UninstallPlugin( if declaration.Tool != nil { toolInstallation := &models.ToolInstallation{ PluginID: pluginToBeReturns.PluginID, - TenantID: tenant_id, + TenantID: tenantId, } err := db.DeleteByCondition(&toolInstallation, tx) @@ -263,7 +263,7 @@ func UninstallPlugin( if declaration.AgentStrategy != nil { agentStrategyInstallation := &models.AgentStrategyInstallation{ PluginID: pluginToBeReturns.PluginID, - TenantID: tenant_id, + TenantID: tenantId, } err := db.DeleteByCondition(&agentStrategyInstallation, tx) @@ -276,7 +276,7 @@ func UninstallPlugin( if declaration.Model != nil { modelInstallation := &models.AIModelInstallation{ PluginID: pluginToBeReturns.PluginID, - TenantID: tenant_id, + TenantID: tenantId, } err := db.DeleteByCondition(&modelInstallation, tx) @@ -318,12 +318,12 @@ type UpgradePluginResponse struct { // and uninstall the original plugin and install the new plugin, but keep the original installation information // like endpoint_setups, etc. func UpgradePlugin( - tenant_id string, - original_plugin_unique_identifier plugin_entities.PluginUniqueIdentifier, - new_plugin_unique_identifier plugin_entities.PluginUniqueIdentifier, + tenantId string, + originalPluginUniqueIdentifier plugin_entities.PluginUniqueIdentifier, + newPluginUniqueIdentifier plugin_entities.PluginUniqueIdentifier, originalDeclaration *plugin_entities.PluginDeclaration, newDeclaration *plugin_entities.PluginDeclaration, - install_type plugin_entities.PluginRuntimeType, + installType plugin_entities.PluginRuntimeType, source string, meta map[string]any, ) (*UpgradePluginResponse, error) { @@ -332,8 +332,8 @@ func UpgradePlugin( err := db.WithTransaction(func(tx *gorm.DB) error { installation, err := db.GetOne[models.PluginInstallation]( db.WithTransactionContext(tx), - db.Equal("plugin_unique_identifier", original_plugin_unique_identifier.String()), - db.Equal("tenant_id", tenant_id), + db.Equal("plugin_unique_identifier", originalPluginUniqueIdentifier.String()), + db.Equal("tenant_id", tenantId), db.WLock(), ) @@ -346,15 +346,15 @@ func UpgradePlugin( // check if the new plugin has existed plugin, err := db.GetOne[models.Plugin]( db.WithTransactionContext(tx), - db.Equal("plugin_unique_identifier", new_plugin_unique_identifier.String()), + db.Equal("plugin_unique_identifier", newPluginUniqueIdentifier.String()), ) if err == db.ErrDatabaseNotFound { // create new plugin plugin = models.Plugin{ - PluginID: new_plugin_unique_identifier.PluginID(), - PluginUniqueIdentifier: new_plugin_unique_identifier.String(), - InstallType: install_type, + PluginID: newPluginUniqueIdentifier.PluginID(), + PluginUniqueIdentifier: newPluginUniqueIdentifier.String(), + InstallType: installType, Refers: 0, ManifestType: manifest_entities.PluginType, } @@ -368,7 +368,7 @@ func UpgradePlugin( } // update exists installation - installation.PluginUniqueIdentifier = new_plugin_unique_identifier.String() + installation.PluginUniqueIdentifier = newPluginUniqueIdentifier.String() installation.Meta = meta err = db.Update(installation, tx) if err != nil { @@ -379,7 +379,7 @@ func UpgradePlugin( err = db.Run( db.WithTransactionContext(tx), db.Model(&models.Plugin{}), - db.Equal("plugin_unique_identifier", original_plugin_unique_identifier.String()), + db.Equal("plugin_unique_identifier", originalPluginUniqueIdentifier.String()), db.Inc(map[string]int{"refers": -1}), ) @@ -390,7 +390,7 @@ func UpgradePlugin( // delete the original plugin if the refers is 0 originalPlugin, err := db.GetOne[models.Plugin]( db.WithTransactionContext(tx), - db.Equal("plugin_unique_identifier", original_plugin_unique_identifier.String()), + db.Equal("plugin_unique_identifier", originalPluginUniqueIdentifier.String()), ) if err == nil && originalPlugin.Refers == 0 { @@ -408,7 +408,7 @@ func UpgradePlugin( err = db.Run( db.WithTransactionContext(tx), db.Model(&models.Plugin{}), - db.Equal("plugin_unique_identifier", new_plugin_unique_identifier.String()), + db.Equal("plugin_unique_identifier", newPluginUniqueIdentifier.String()), db.Inc(map[string]int{"refers": 1}), ) @@ -420,8 +420,8 @@ func UpgradePlugin( if originalDeclaration.Model != nil { // delete the original ai model installation err := db.DeleteByCondition(&models.AIModelInstallation{ - PluginID: original_plugin_unique_identifier.PluginID(), - TenantID: tenant_id, + PluginID: originalPluginUniqueIdentifier.PluginID(), + TenantID: tenantId, }, tx) if err != nil { @@ -432,10 +432,10 @@ func UpgradePlugin( if newDeclaration.Model != nil { // create the new ai model installation modelInstallation := &models.AIModelInstallation{ - PluginUniqueIdentifier: new_plugin_unique_identifier.String(), - TenantID: tenant_id, + PluginUniqueIdentifier: newPluginUniqueIdentifier.String(), + TenantID: tenantId, Provider: newDeclaration.Model.Provider, - PluginID: new_plugin_unique_identifier.PluginID(), + PluginID: newPluginUniqueIdentifier.PluginID(), } err := db.Create(modelInstallation, tx) @@ -448,8 +448,8 @@ func UpgradePlugin( if originalDeclaration.Tool != nil { // delete the original tool installation err := db.DeleteByCondition(&models.ToolInstallation{ - PluginID: original_plugin_unique_identifier.PluginID(), - TenantID: tenant_id, + PluginID: originalPluginUniqueIdentifier.PluginID(), + TenantID: tenantId, }, tx) if err != nil { @@ -460,10 +460,10 @@ func UpgradePlugin( if newDeclaration.Tool != nil { // create the new tool installation toolInstallation := &models.ToolInstallation{ - PluginUniqueIdentifier: new_plugin_unique_identifier.String(), - TenantID: tenant_id, + PluginUniqueIdentifier: newPluginUniqueIdentifier.String(), + TenantID: tenantId, Provider: newDeclaration.Tool.Identity.Name, - PluginID: new_plugin_unique_identifier.PluginID(), + PluginID: newPluginUniqueIdentifier.PluginID(), } err := db.Create(toolInstallation, tx) @@ -476,8 +476,8 @@ func UpgradePlugin( if originalDeclaration.AgentStrategy != nil { // delete the original agent installation err := db.DeleteByCondition(&models.AgentStrategyInstallation{ - PluginID: original_plugin_unique_identifier.PluginID(), - TenantID: tenant_id, + PluginID: originalPluginUniqueIdentifier.PluginID(), + TenantID: tenantId, }, tx) if err != nil { @@ -488,10 +488,10 @@ func UpgradePlugin( if newDeclaration.AgentStrategy != nil { // create the new agent installation agentStrategyInstallation := &models.AgentStrategyInstallation{ - PluginUniqueIdentifier: new_plugin_unique_identifier.String(), - TenantID: tenant_id, + PluginUniqueIdentifier: newPluginUniqueIdentifier.String(), + TenantID: tenantId, Provider: newDeclaration.AgentStrategy.Identity.Name, - PluginID: new_plugin_unique_identifier.PluginID(), + PluginID: newPluginUniqueIdentifier.PluginID(), } err := db.Create(agentStrategyInstallation, tx) diff --git a/internal/types/models/task.go b/internal/types/models/task.go index 957d318d0a..0dc384a1a9 100644 --- a/internal/types/models/task.go +++ b/internal/types/models/task.go @@ -15,6 +15,7 @@ type InstallTaskPluginStatus struct { PluginUniqueIdentifier plugin_entities.PluginUniqueIdentifier `json:"plugin_unique_identifier"` Labels plugin_entities.I18nObject `json:"labels"` Icon string `json:"icon"` + IconDark string `json:"icon_dark"` PluginID string `json:"plugin_id"` Status InstallTaskStatus `json:"status"` Message string `json:"message"` diff --git a/internal/utils/cache/cache.go b/internal/utils/cache/cache.go index fdb8ac6efd..051189ecda 100644 --- a/internal/utils/cache/cache.go +++ b/internal/utils/cache/cache.go @@ -29,6 +29,15 @@ type Client interface { Transaction(fn func(context Context) error) error Publish(channel string, message string) error Subscribe(channel string) (<-chan string, func()) + // Additional methods from the original redis.go + Increase(key string) (int64, error) + Decrease(key string) (int64, error) + SetExpire(key string, time time.Duration) error + ScanKeys(match string) ([]string, error) + ScanKeysAsync(match string, fn func([]string) error) error + SetMapFields(key string, v map[string]any) error + Lock(key string, expire time.Duration, tryLockTimeout time.Duration) error + Unlock(key string) error } var ( @@ -276,44 +285,6 @@ func SetNX[T any](key string, value T, expire time.Duration, context ...Context) return getCmdable(context...).SetNX(serialKey(key), bytes, expire) } -var ( - ErrLockTimeout = errors.New("lock timeout") -) - -// Lock key, expire time takes responsibility for expiration time -// try_lock_timeout takes responsibility for the timeout of trying to lock -func Lock(key string, expire time.Duration, tryLockTimeout time.Duration, context ...Context) error { - if client == nil { - return ErrNotInit - } - - const LOCK_DURATION = 20 * time.Millisecond - - ticker := time.NewTicker(LOCK_DURATION) - defer ticker.Stop() - - for range ticker.C { - if _, err := getCmdable(context...).SetNX(serialKey(key), "1", expire); err == nil { - return nil - } - - tryLockTimeout -= LOCK_DURATION - if tryLockTimeout <= 0 { - return ErrLockTimeout - } - } - - return nil -} - -func Unlock(key string, context ...Context) error { - if client == nil { - return ErrNotInit - } - - _, err := getCmdable(context...).Delete(serialKey(key)) - return err -} func Expire(key string, time time.Duration, context ...Context) (bool, error) { if client == nil { @@ -360,3 +331,88 @@ func Subscribe[T any](channel string) (<-chan T, func()) { return ch, fn } + +// Increase increases the key value by 1 +func Increase(key string, context ...Context) (int64, error) { + if client == nil { + return 0, ErrNotInit + } + + return getCmdable(context...).Increase(serialKey(key)) +} + +// Decrease decreases the key value by 1 +func Decrease(key string, context ...Context) (int64, error) { + if client == nil { + return 0, ErrNotInit + } + + return getCmdable(context...).Decrease(serialKey(key)) +} + +// SetExpire sets the expire time for the key +func SetExpire(key string, time time.Duration, context ...Context) error { + if client == nil { + return ErrNotInit + } + + return getCmdable(context...).SetExpire(serialKey(key), time) +} + +// ScanKeys scans keys with match pattern +func ScanKeys(match string, context ...Context) ([]string, error) { + if client == nil { + return nil, ErrNotInit + } + + result := make([]string, 0) + + if err := ScanKeysAsync(match, func(keys []string) error { + result = append(result, keys...) + return nil + }, context...); err != nil { + return nil, err + } + + return result, nil +} + +// ScanKeysAsync scans keys with match pattern asynchronously +func ScanKeysAsync(match string, fn func([]string) error, context ...Context) error { + if client == nil { + return ErrNotInit + } + + return getCmdable(context...).ScanKeysAsync(serialKey(match), fn) +} + +// SetMapFields sets multiple map fields at once +func SetMapFields(key string, v map[string]any, context ...Context) error { + if client == nil { + return ErrNotInit + } + + return getCmdable(context...).SetMapFields(serialKey(key), v) +} + +var ( + ErrLockTimeout = errors.New("lock timeout") +) + +// Lock implements distributed locking +func Lock(key string, expire time.Duration, tryLockTimeout time.Duration, context ...Context) error { + if client == nil { + return ErrNotInit + } + + return getCmdable(context...).Lock(serialKey(key), expire, tryLockTimeout) +} + +// Unlock releases the distributed lock +func Unlock(key string, context ...Context) error { + if client == nil { + return ErrNotInit + } + + return getCmdable(context...).Unlock(serialKey(key)) +} diff --git a/internal/utils/cache/mysql/mysql.go b/internal/utils/cache/mysql/mysql.go index 92bfd3160a..bd02325aea 100644 --- a/internal/utils/cache/mysql/mysql.go +++ b/internal/utils/cache/mysql/mysql.go @@ -67,7 +67,7 @@ func (c Client) Set(key string, value any, expire time.Duration) error { val := toBytes(value) expireTime := time.Now().Add(expire) - // 使用 INSERT ... ON DUPLICATE KEY UPDATE 来避免并发写入问题 + // Use INSERT ... ON DUPLICATE KEY UPDATE to avoid concurrent write issues sql := `INSERT INTO cache_kvs (cache_key, cache_value, expire_time, created_at, updated_at) VALUES (?, ?, ?, NOW(), NOW()) ON DUPLICATE KEY UPDATE @@ -117,7 +117,7 @@ func (c Client) Count(key ...string) (int64, error) { } func (c Client) SetMapField(key string, field string, value string) error { - // 使用 INSERT ... ON DUPLICATE KEY UPDATE 来避免并发写入问题 + // Use INSERT ... ON DUPLICATE KEY UPDATE to avoid concurrent write issues sql := `INSERT INTO cache_maps (cache_key, cache_field, cache_value, created_at, updated_at) VALUES (?, ?, ?, NOW(), NOW()) ON DUPLICATE KEY UPDATE @@ -197,7 +197,7 @@ func (c Client) SetNX(key string, value any, expire time.Duration) (bool, error) val := toBytes(value) expireTime := time.Now().Add(expire) - // 使用 INSERT IGNORE 来实现 SetNX,避免并发写入问题 + // Use INSERT IGNORE to implement SetNX, avoiding concurrent write issues sql := `INSERT IGNORE INTO cache_kvs (cache_key, cache_value, expire_time, created_at, updated_at) VALUES (?, ?, ?, NOW(), NOW())` @@ -206,7 +206,7 @@ func (c Client) SetNX(key string, value any, expire time.Duration) (bool, error) return false, result.Error } - // 如果影响的行数为1,说明插入成功;如果为0,说明记录已存在 + // If affected rows is 1, insertion succeeded; if 0, record already exists return result.RowsAffected == 1, nil } @@ -298,3 +298,181 @@ func (c Client) Subscribe(channel string) (<-chan string, func()) { close(stop) } } + +// Increase increases the key value by 1 +func (c Client) Increase(key string) (int64, error) { + // MySQL implementation: first try to get current value, then increment + var cacheKV CacheKV + result := c.DB.Where("cache_key = ? AND expire_time > ?", key, time.Now()).First(&cacheKV) + if result.Error != nil { + if result.Error.Error() == "record not found" { + // If not exists, create new record with value 1 + expireTime := time.Now().Add(time.Hour * 24) // Default 24 hours expiration + sql := `INSERT INTO cache_kvs (cache_key, cache_value, expire_time, created_at, updated_at) + VALUES (?, ?, ?, NOW(), NOW()) + ON DUPLICATE KEY UPDATE + cache_value = CAST(cache_value AS UNSIGNED) + 1, + updated_at = NOW()` + err := c.DB.Exec(sql, key, []byte("1"), expireTime).Error + if err != nil { + return 0, err + } + return 1, nil + } + return 0, result.Error + } + + // Increment existing value + sql := `UPDATE cache_kvs + SET cache_value = CAST(cache_value AS UNSIGNED) + 1, + updated_at = NOW() + WHERE cache_key = ? AND expire_time > ?` + result = c.DB.Exec(sql, key, time.Now()) + if result.Error != nil { + return 0, result.Error + } + + // Get new value + var newValue int64 + err := c.DB.Model(&CacheKV{}). + Select("CAST(cache_value AS UNSIGNED)"). + Where("cache_key = ? AND expire_time > ?", key, time.Now()). + Scan(&newValue).Error + if err != nil { + return 0, err + } + + return newValue, nil +} + +// Decrease decreases the key value by 1 +func (c Client) Decrease(key string) (int64, error) { + // MySQL implementation: first try to get current value, then decrement + var cacheKV CacheKV + result := c.DB.Where("cache_key = ? AND expire_time > ?", key, time.Now()).First(&cacheKV) + if result.Error != nil { + if result.Error.Error() == "record not found" { + return 0, cache.ErrNotFound + } + return 0, result.Error + } + + // Decrement existing value + sql := `UPDATE cache_kvs + SET cache_value = CAST(cache_value AS UNSIGNED) - 1, + updated_at = NOW() + WHERE cache_key = ? AND expire_time > ?` + result = c.DB.Exec(sql, key, time.Now()) + if result.Error != nil { + return 0, result.Error + } + + // Get new value + var newValue int64 + err := c.DB.Model(&CacheKV{}). + Select("CAST(cache_value AS UNSIGNED)"). + Where("cache_key = ? AND expire_time > ?", key, time.Now()). + Scan(&newValue).Error + if err != nil { + return 0, err + } + + return newValue, nil +} + +// SetExpire sets the expire time for the key +func (c Client) SetExpire(key string, expire time.Duration) error { + expireTime := time.Now().Add(expire) + result := c.DB.Model(&CacheKV{}). + Where("cache_key = ?", key). + Update("expire_time", expireTime) + return result.Error +} + +// ScanKeys scans keys with match pattern +func (c Client) ScanKeys(match string) ([]string, error) { + var cacheKVs []CacheKV + query := c.DB.Model(&CacheKV{}).Where("expire_time > ?", time.Now()) + + if match != "" { + sqlPattern := convertRegexToSQL(match) + query = query.Where("cache_key LIKE ?", sqlPattern) + } + + result := query.Find(&cacheKVs) + if result.Error != nil { + return nil, result.Error + } + + var keys []string + for _, cacheKV := range cacheKVs { + keys = append(keys, cacheKV.CacheKey) + } + + return keys, nil +} + +// ScanKeysAsync scans keys with match pattern asynchronously +func (c Client) ScanKeysAsync(match string, fn func([]string) error) error { + keys, err := c.ScanKeys(match) + if err != nil { + return err + } + return fn(keys) +} + +// SetMapFields sets multiple map fields at once +func (c Client) SetMapFields(key string, v map[string]any) error { + // MySQL implementation: batch insert or update + for field, value := range v { + valueStr := fmt.Sprintf("%v", value) + err := c.SetMapField(key, field, valueStr) + if err != nil { + return err + } + } + return nil +} + +// Lock implements distributed locking +func (c Client) Lock(key string, expire time.Duration, tryLockTimeout time.Duration) error { + lockKey := fmt.Sprintf("lock:%s", key) + + // Try to acquire lock + success, err := c.SetNX(lockKey, "1", expire) + if err != nil { + return err + } + + if success { + return nil + } + + // If acquisition fails, wait and retry + ticker := time.NewTicker(20 * time.Millisecond) + defer ticker.Stop() + + for range ticker.C { + success, err := c.SetNX(lockKey, "1", expire) + if err != nil { + return err + } + if success { + return nil + } + + tryLockTimeout -= 20 * time.Millisecond + if tryLockTimeout <= 0 { + return cache.ErrNotFound // Use existing error type + } + } + + return nil +} + +// Unlock releases the distributed lock +func (c Client) Unlock(key string) error { + lockKey := fmt.Sprintf("lock:%s", key) + _, err := c.Delete(lockKey) + return err +} diff --git a/internal/utils/cache/mysql/mysql_test.go b/internal/utils/cache/mysql/mysql_test.go index b48c8126d5..8de01c0c3b 100644 --- a/internal/utils/cache/mysql/mysql_test.go +++ b/internal/utils/cache/mysql/mysql_test.go @@ -30,18 +30,18 @@ func init() { DBSslMode: "disable", } var err error - db.DifyPluginDB, err = mysql.InitPluginDB( - config.DBHost, - int(config.DBPort), - config.DBDatabase, - config.DBDefaultDatabase, - config.DBUsername, - config.DBPassword, - config.DBSslMode, - config.DBMaxIdleConns, - config.DBMaxOpenConns, - config.DBConnMaxLifetime, - ) + db.DifyPluginDB, err = mysql.InitPluginDB(&mysql.MySQLConfig{ + Host: config.DBHost, + Port: int(config.DBPort), + DBName: config.DBDatabase, + DefaultDBName: config.DBDefaultDatabase, + User: config.DBUsername, + Pass: config.DBPassword, + SSLMode: config.DBSslMode, + MaxIdleConns: config.DBMaxIdleConns, + MaxOpenConns: config.DBMaxOpenConns, + ConnMaxLifetime: config.DBConnMaxLifetime, + }) if err != nil { log.Panicf("failed init plugin db: %v", err) } diff --git a/internal/utils/cache/redis/redis.go b/internal/utils/cache/redis/redis.go index cbe18281f6..4d5838b542 100644 --- a/internal/utils/cache/redis/redis.go +++ b/internal/utils/cache/redis/redis.go @@ -3,6 +3,7 @@ package redis import ( "context" "crypto/tls" + "errors" "time" "github.com/langgenius/dify-plugin-daemon/internal/utils/cache" @@ -49,6 +50,35 @@ func InitRedisClient(addr, username, password string, useSsl bool, db int) error return nil } +func InitRedisSentinelClient(sentinels []string, masterName, username, password, sentinelUsername, sentinelPassword string, useSsl bool, db int, socketTimeout float64) error { + opts := &redis.FailoverOptions{ + MasterName: masterName, + SentinelAddrs: sentinels, + Username: username, + Password: password, + DB: db, + SentinelUsername: sentinelUsername, + SentinelPassword: sentinelPassword, + } + + if useSsl { + opts.TLSConfig = &tls.Config{} + } + + if socketTimeout > 0 { + opts.DialTimeout = time.Duration(socketTimeout * float64(time.Second)) + } + + client := redis.NewFailoverClient(opts) + + if _, err := client.Ping(ctx).Result(); err != nil { + return err + } + + cache.SetClient(&Client{client}) + return nil +} + func (c *Client) Close() error { client := c.Cmdable.(*redis.Client) return client.Close() @@ -86,6 +116,11 @@ func (c *Client) SetMapField(key string, field string, value string) error { return c.Cmdable.HSet(ctx, key, field, value).Err() } +// SetMapFields sets multiple map fields at once +func (c *Client) SetMapFields(key string, v map[string]any) error { + return c.Cmdable.HMSet(ctx, key, v).Err() +} + func (c *Client) GetMapField(key string, field string) (string, error) { val, err := c.Cmdable.HGet(ctx, key, field).Result() if err != nil && err == redis.Nil { @@ -118,6 +153,104 @@ func (c *Client) Expire(key string, time time.Duration) (bool, error) { return c.Cmdable.Expire(ctx, key, time).Result() } +// Increase increases the key value by 1 +func (c *Client) Increase(key string) (int64, error) { + num, err := c.Cmdable.Incr(ctx, key).Result() + if err != nil && err == redis.Nil { + return 0, cache.ErrNotFound + } + return num, err +} + +// Decrease decreases the key value by 1 +func (c *Client) Decrease(key string) (int64, error) { + return c.Cmdable.Decr(ctx, key).Result() +} + +// SetExpire sets the expire time for the key +func (c *Client) SetExpire(key string, time time.Duration) error { + return c.Cmdable.Expire(ctx, key, time).Err() +} + +// ScanKeys scans keys with match pattern +func (c *Client) ScanKeys(match string) ([]string, error) { + result := make([]string, 0) + cursor := uint64(0) + + for { + keys, newCursor, err := c.Cmdable.Scan(ctx, cursor, match, 32).Result() + if err != nil { + return nil, err + } + + result = append(result, keys...) + + if newCursor == 0 { + break + } + + cursor = newCursor + } + + return result, nil +} + +// ScanKeysAsync scans keys with match pattern asynchronously +func (c *Client) ScanKeysAsync(match string, fn func([]string) error) error { + cursor := uint64(0) + + for { + keys, newCursor, err := c.Cmdable.Scan(ctx, cursor, match, 32).Result() + if err != nil { + return err + } + + if err := fn(keys); err != nil { + return err + } + + if newCursor == 0 { + break + } + + cursor = newCursor + } + + return nil +} + +var ( + ErrLockTimeout = errors.New("lock timeout") +) + +// Lock implements distributed locking +func (c *Client) Lock(key string, expire time.Duration, tryLockTimeout time.Duration) error { + const LOCK_DURATION = 20 * time.Millisecond + + ticker := time.NewTicker(LOCK_DURATION) + defer ticker.Stop() + + for range ticker.C { + if success, err := c.Cmdable.SetNX(ctx, key, "1", expire).Result(); err != nil { + return err + } else if success { + return nil + } + + tryLockTimeout -= LOCK_DURATION + if tryLockTimeout <= 0 { + return ErrLockTimeout + } + } + + return nil +} + +// Unlock releases the distributed lock +func (c *Client) Unlock(key string) error { + return c.Cmdable.Del(ctx, key).Err() +} + func (c *Client) Transaction(fn func(context cache.Context) error) error { client := c.Cmdable.(*redis.Client) return client.Watch(ctx, func(tx *redis.Tx) error { diff --git a/internal/utils/cache/redis/redis_test.go b/internal/utils/cache/redis/redis_test.go index dfacf5ad18..de120350b5 100644 --- a/internal/utils/cache/redis/redis_test.go +++ b/internal/utils/cache/redis/redis_test.go @@ -2,12 +2,15 @@ package redis import ( "errors" + "fmt" "strings" "sync" + "sync/atomic" "testing" "time" "github.com/langgenius/dify-plugin-daemon/internal/utils/cache" + "github.com/stretchr/testify/assert" ) const ( @@ -313,3 +316,39 @@ func TestSetAndGet(t *testing.T) { t.Fatalf("Get[\"key\"] should be ErrNotFound") } } + +func TestLock(t *testing.T) { + if err := InitRedisClient("127.0.0.1:6379", "", "difyai123456", false, 0); err != nil { + t.Fatal(err) + } + defer cache.Close() + + const CONCURRENCY = 10 + const SINGLE_TURN_TIME = 100 + + wg := sync.WaitGroup{} + wg.Add(CONCURRENCY) + + waitMilliseconds := int32(0) + + foo := func() { + cache.Lock("test-lock", SINGLE_TURN_TIME*time.Millisecond*1000, SINGLE_TURN_TIME*time.Millisecond*1000) + started := time.Now() + time.Sleep(SINGLE_TURN_TIME * time.Millisecond) + defer func() { + cache.Unlock("test-lock") + atomic.AddInt32(&waitMilliseconds, int32(time.Since(started).Milliseconds())) + wg.Done() + }() + } + + for range CONCURRENCY { + go foo() + } + + wg.Wait() + + fmt.Println("waitSeconds", waitMilliseconds) + + assert.GreaterOrEqual(t, waitMilliseconds, int32(100*CONCURRENCY)) +} diff --git a/internal/utils/http_requests/http_options.go b/internal/utils/http_requests/http_options.go index 2b2b5d13e6..041f2c5e0c 100644 --- a/internal/utils/http_requests/http_options.go +++ b/internal/utils/http_requests/http_options.go @@ -7,38 +7,58 @@ type HttpOptions struct { Value interface{} } +const ( + HttpOptionTypeWriteTimeout = "write_timeout" + HttpOptionTypeReadTimeout = "read_timeout" + HttpOptionTypeHeader = "header" + HttpOptionTypeParams = "params" + HttpOptionTypePayload = "payload" + HttpOptionTypePayloadText = "payloadText" + HttpOptionTypePayloadJson = "payloadJson" + HttpOptionTypePayloadMultipart = "payloadMultipart" + HttpOptionTypeRaiseErrorWhenStreamDataNotMatch = "raiseErrorWhenStreamDataNotMatch" + HttpOptionTypeDirectReferer = "directReferer" + HttpOptionTypeRetCode = "retCode" + HttpOptionTypeUsingLengthPrefixed = "usingLengthPrefixed" +) + // milliseconds func HttpWriteTimeout(timeout int64) HttpOptions { - return HttpOptions{"write_timeout", timeout} + return HttpOptions{HttpOptionTypeWriteTimeout, timeout} } // milliseconds func HttpReadTimeout(timeout int64) HttpOptions { - return HttpOptions{"read_timeout", timeout} + return HttpOptions{HttpOptionTypeReadTimeout, timeout} } func HttpHeader(header map[string]string) HttpOptions { - return HttpOptions{"header", header} + return HttpOptions{HttpOptionTypeHeader, header} } // which is used for params with in url func HttpParams(params map[string]string) HttpOptions { - return HttpOptions{"params", params} + return HttpOptions{HttpOptionTypeParams, params} } // which is used for POST method only func HttpPayload(payload map[string]string) HttpOptions { - return HttpOptions{"payload", payload} + return HttpOptions{HttpOptionTypePayload, payload} } // which is used for POST method only func HttpPayloadText(payload string) HttpOptions { - return HttpOptions{"payloadText", payload} + return HttpOptions{HttpOptionTypePayloadText, payload} +} + +// which is used for POST method only +func HttpPayloadReader(reader io.ReadCloser) HttpOptions { + return HttpOptions{"payloadReader", reader} } // which is used for POST method only func HttpPayloadJson(payload interface{}) HttpOptions { - return HttpOptions{"payloadJson", payload} + return HttpOptions{HttpOptionTypePayloadJson, payload} } type HttpPayloadMultipartFile struct { @@ -49,20 +69,43 @@ type HttpPayloadMultipartFile struct { // which is used for POST method only // payload follows the form data format, and files is a map from filename to file func HttpPayloadMultipart(payload map[string]string, files map[string]HttpPayloadMultipartFile) HttpOptions { - return HttpOptions{"payloadMultipart", map[string]interface{}{ + return HttpOptions{HttpOptionTypePayloadMultipart, map[string]interface{}{ "payload": payload, "files": files, }} } func HttpRaiseErrorWhenStreamDataNotMatch(raise bool) HttpOptions { - return HttpOptions{"raiseErrorWhenStreamDataNotMatch", raise} + return HttpOptions{HttpOptionTypeRaiseErrorWhenStreamDataNotMatch, raise} } func HttpWithDirectReferer() HttpOptions { - return HttpOptions{"directReferer", true} + return HttpOptions{HttpOptionTypeDirectReferer, true} } func HttpWithRetCode(retCode *int) HttpOptions { - return HttpOptions{"retCode", retCode} + return HttpOptions{HttpOptionTypeRetCode, retCode} +} + +// For standard SSE protocol, response are split by \n\n +// Which leads a bad performance when decoding, we need a larger chunk to store temporary data +// This option is used to enable length-prefixed mode, which is faster but less memory-friendly +// We uses following format: +// +// | Field | Size | Description | +// |---------------|----------|---------------------------------| +// | Magic Number | 1 byte | Magic number identifier | +// | Reserved | 1 byte | Reserved field | +// | Header Length | 2 bytes | Header length (usually 0xa) | +// | Data Length | 4 bytes | Length of the data | +// | Reserved | 6 bytes | Reserved fields | +// | Data | Variable | Actual data content | +// +// | Reserved Fields | Header | Data | +// |-----------------|----------|----------| +// | 4 bytes total | Variable | Variable | +// +// with the above format, we can achieve a better performance, avoid unexpected memory growth +func HttpUsingLengthPrefixed(using bool) HttpOptions { + return HttpOptions{HttpOptionTypeUsingLengthPrefixed, using} } diff --git a/internal/utils/http_requests/http_request.go b/internal/utils/http_requests/http_request.go index b8de61b68c..5070dcffcf 100644 --- a/internal/utils/http_requests/http_request.go +++ b/internal/utils/http_requests/http_request.go @@ -2,13 +2,11 @@ package http_requests import ( "bytes" - "context" "encoding/json" "io" "mime/multipart" "net/http" "strings" - "time" ) func buildHttpRequest(method string, url string, options ...HttpOptions) (*http.Request, error) { @@ -19,11 +17,6 @@ func buildHttpRequest(method string, url string, options ...HttpOptions) (*http. for _, option := range options { switch option.Type { - case "write_timeout": - timeout := time.Second * time.Duration(option.Value.(int64)) - ctx, cancel := context.WithTimeout(context.Background(), timeout) - time.AfterFunc(timeout, cancel) // release resources associated with context asynchronously - req = req.WithContext(ctx) case "header": for k, v := range option.Value.(map[string]string) { req.Header.Set(k, v) @@ -75,6 +68,8 @@ func buildHttpRequest(method string, url string, options ...HttpOptions) (*http. case "payloadText": req.Body = io.NopCloser(strings.NewReader(option.Value.(string))) req.Header.Set("Content-Type", "text/plain") + case "payloadReader": + req.Body = option.Value.(io.ReadCloser) case "payloadJson": jsonStr, err := json.Marshal(option.Value) if err != nil { diff --git a/internal/utils/http_requests/http_warpper.go b/internal/utils/http_requests/http_warpper.go index 99518e15cc..c462ab1d2c 100644 --- a/internal/utils/http_requests/http_warpper.go +++ b/internal/utils/http_requests/http_warpper.go @@ -1,7 +1,6 @@ package http_requests import ( - "bufio" "bytes" "encoding/json" "fmt" @@ -37,7 +36,7 @@ func RequestAndParse[T any](client *http.Client, url string, method string, opti // get read timeout readTimeout := int64(60000) for _, option := range options { - if option.Type == "read_timeout" { + if option.Type == HttpOptionTypeReadTimeout { readTimeout = option.Value.(int64) break } @@ -92,12 +91,14 @@ func RequestAndParseStream[T any](client *http.Client, url string, method string // get read timeout readTimeout := int64(60000) raiseErrorWhenStreamDataNotMatch := false + usingLengthPrefixed := false for _, option := range options { - if option.Type == "read_timeout" { + if option.Type == HttpOptionTypeReadTimeout { readTimeout = option.Value.(int64) - break - } else if option.Type == "raiseErrorWhenStreamDataNotMatch" { + } else if option.Type == HttpOptionTypeRaiseErrorWhenStreamDataNotMatch { raiseErrorWhenStreamDataNotMatch = option.Value.(bool) + } else if option.Type == HttpOptionTypeUsingLengthPrefixed { + usingLengthPrefixed = option.Value.(bool) } } time.AfterFunc(time.Millisecond*time.Duration(readTimeout), func() { @@ -105,45 +106,58 @@ func RequestAndParseStream[T any](client *http.Client, url string, method string resp.Body.Close() }) + // Common data processor function to reduce code duplication + processData := func(data []byte) error { + // unmarshal + t, err := parser.UnmarshalJsonBytes[T](data) + if err != nil { + if raiseErrorWhenStreamDataNotMatch { + return err + } else { + log.Warn("stream data not match for %s, got %s", url, string(data)) + return nil + } + } + + ch.Write(t) + return nil + } + routine.Submit(map[string]string{ "module": "http_requests", "function": "RequestAndParseStream", }, func() { - scanner := bufio.NewScanner(resp.Body) defer resp.Body.Close() - for scanner.Scan() { - data := scanner.Bytes() - if len(data) == 0 { - continue - } - - if bytes.HasPrefix(data, []byte("data:")) { - // split - data = data[5:] - } + var err error + if usingLengthPrefixed { + // at most 30MB a single chunk + err = parser.LengthPrefixedChunking(resp.Body, 0x0f, 1024*1024*30, processData) + } else { + err = parser.LineBasedChunking(resp.Body, 1024*1024*30, func(data []byte) error { + if len(data) == 0 { + return nil + } - if bytes.HasPrefix(data, []byte("event:")) { - // TODO: handle event - continue - } + if bytes.HasPrefix(data, []byte("data:")) { + // split + data = data[5:] + } - // trim space - data = bytes.TrimSpace(data) - - // unmarshal - t, err := parser.UnmarshalJsonBytes[T](data) - if err != nil { - if raiseErrorWhenStreamDataNotMatch { - ch.WriteError(err) - break - } else { - log.Warn("stream data not match for %s, got %s", url, string(data)) + if bytes.HasPrefix(data, []byte("event:")) { + // TODO: handle event + return nil } - continue - } - ch.Write(t) + // trim space + data = bytes.TrimSpace(data) + + return processData(data) + }) + } + + if err != nil { + ch.WriteError(err) } ch.Close() diff --git a/internal/utils/parser/chunking.go b/internal/utils/parser/chunking.go new file mode 100644 index 0000000000..691b60c5e8 --- /dev/null +++ b/internal/utils/parser/chunking.go @@ -0,0 +1,107 @@ +package parser + +import ( + "bufio" + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" +) + +func LineBasedChunking(reader io.Reader, maxChunkSize int, processor func([]byte) error) error { + scanner := bufio.NewScanner(reader) + scanner.Buffer(make([]byte, 1024), maxChunkSize) + scanner.Split(bufio.ScanLines) + + for scanner.Scan() { + line := scanner.Bytes() + if len(line) > maxChunkSize { + return fmt.Errorf("line is too long: %d", len(line)) + } + + if err := processor(line); err != nil { + return err + } + } + return nil +} + +// We uses following format: +// All data is stored in little endian format +// +// | Field | Size | Description | +// |---------------|----------|---------------------------------| +// | Magic Number | 1 byte | Magic number identifier | +// | Reserved | 1 byte | Reserved field | +// | Header Length | 2 bytes | Header length (usually 0xa) | +// | Data Length | 4 bytes | Length of the data | +// | Reserved | 6 bytes | Reserved fields | +// | Data | Variable | Actual data content | +// +// | Reserved Fields | Header | Data | +// |-----------------|----------|----------| +// | 4 bytes total | Variable | Variable | +// +// NOTE: this function is not thread safe +func LengthPrefixedChunking( + reader io.Reader, + magicNumber byte, + maxChunkSize uint32, + processor func([]byte) error, +) error { + // read until EOF + buf := bytes.NewBuffer(nil) + + for { + // read length + length := make([]byte, 4) + _, err := io.ReadFull(reader, length) + if err != nil { + if err == io.EOF { + return nil // Normal EOF, processing complete + } + return errors.Join(err, fmt.Errorf("failed to read system header")) + } + + // check magic number + if length[0] != magicNumber { + return fmt.Errorf("magic number mismatch: %d", length[0]) + } + + // read header length + headerLength := binary.LittleEndian.Uint16(length[2:4]) + if headerLength != 0xa { + return fmt.Errorf("header length mismatch: %d", headerLength) + } + + // read header + header := make([]byte, headerLength) + _, err = io.ReadFull(reader, header) + if err != nil { + return errors.Join(err, fmt.Errorf("failed to read header")) + } + + // decoding data length + dataLength := binary.LittleEndian.Uint32(header[:4]) + if dataLength > maxChunkSize { + return fmt.Errorf("data length is too long: %d", dataLength) + } + + // Reset buffer for new data + buf.Reset() + + // Read data into buffer + // io.CopyN will not return io.EOF if dataLength equals to actual data length + _, err = io.CopyN(buf, reader, int64(dataLength)) + if err != nil { + return errors.Join(err, fmt.Errorf("failed to read data")) + } + + // Process the data + err = processor(buf.Bytes()) + if err != nil { + return errors.Join(err, fmt.Errorf("failed to process data")) + } + } +} diff --git a/internal/utils/parser/chunking_test.go b/internal/utils/parser/chunking_test.go new file mode 100644 index 0000000000..aca4a8e966 --- /dev/null +++ b/internal/utils/parser/chunking_test.go @@ -0,0 +1,268 @@ +package parser + +import ( + "bytes" + "encoding/binary" + "testing" +) + +func TestLineBasedChunking(t *testing.T) { + tests := []struct { + name string + input string + maxChunkSize int + expected []string + expectError bool + }{ + { + name: "simple lines", + input: "line1\nline2\nline3", + maxChunkSize: 100, + expected: []string{"line1", "line2", "line3"}, + expectError: false, + }, + { + name: "empty lines", + input: "line1\n\nline3", + maxChunkSize: 100, + expected: []string{"line1", "", "line3"}, + expectError: false, + }, + { + name: "single line", + input: "single line", + maxChunkSize: 100, + expected: []string{"single line"}, + expectError: false, + }, + { + name: "line too long", + input: "this is a very long line that exceeds the maximum chunk size", + maxChunkSize: 10, + expected: nil, + expectError: true, + }, + { + name: "empty input", + input: "", + maxChunkSize: 100, + expected: []string{}, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reader := bytes.NewReader([]byte(tt.input)) + var result []string + + err := LineBasedChunking(reader, tt.maxChunkSize, func(data []byte) error { + result = append(result, string(data)) + return nil + }) + + if tt.expectError { + if err == nil { + t.Errorf("expected error but got none") + } + return + } + + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + if len(result) != len(tt.expected) { + t.Errorf("expected %d lines, got %d", len(tt.expected), len(result)) + return + } + + for i, expected := range tt.expected { + if result[i] != expected { + t.Errorf("line %d: expected %q, got %q", i, expected, result[i]) + } + } + }) + } +} + +func TestLengthPrefixedChunking(t *testing.T) { + tests := []struct { + name string + data [][]byte + magicNumber byte + expectError bool + }{ + { + name: "valid single chunk", + data: [][]byte{[]byte("hello world")}, + magicNumber: 0x0f, + expectError: false, + }, + { + name: "valid multiple chunks", + data: [][]byte{[]byte("chunk1"), []byte("chunk2"), []byte("chunk3")}, + magicNumber: 0x0f, + expectError: false, + }, + { + name: "empty chunk", + data: [][]byte{[]byte("")}, + magicNumber: 0x0f, + expectError: false, + }, + { + name: "large chunk", + data: [][]byte{bytes.Repeat([]byte("a"), 1000)}, + magicNumber: 0x0f, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create test data with proper format + var buf bytes.Buffer + for _, chunk := range tt.data { + // Write magic number + buf.WriteByte(tt.magicNumber) + // Write reserved byte + buf.WriteByte(0x00) + // Write header length (0x000a in little endian) + buf.Write([]byte{0x0a, 0x00}) + + // Create header (10 bytes total) + header := make([]byte, 10) + // First 4 bytes are already used for data length placeholder + // Write data length in bytes 4-7 (little endian) + dataLen := uint32(len(chunk)) + binary.LittleEndian.PutUint32(header[:4], dataLen) + // Remaining 6 bytes are reserved (already zero) + + buf.Write(header) + buf.Write(chunk) + } + + reader := bytes.NewReader(buf.Bytes()) + var result [][]byte + + err := LengthPrefixedChunking(reader, tt.magicNumber, 1024*1024, func(data []byte) error { + // Make a copy of the data since it might be reused + chunk := make([]byte, len(data)) + copy(chunk, data) + result = append(result, chunk) + return nil + }) + + if tt.expectError { + if err == nil { + t.Errorf("expected error but got none") + } + return + } + + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + if len(result) != len(tt.data) { + t.Errorf("expected %d chunks, got %d", len(tt.data), len(result)) + return + } + + for i, expected := range tt.data { + if !bytes.Equal(result[i], expected) { + t.Errorf("chunk %d: expected %q, got %q", i, expected, result[i]) + } + } + }) + } +} + +func TestLengthPrefixedChunking_ErrorCases(t *testing.T) { + tests := []struct { + name string + data []byte + magicNumber byte + maxSize uint32 + expectError string + }{ + { + name: "wrong magic number", + data: []byte{0x10, 0x00, 0x0a, 0x00}, // wrong magic number + magicNumber: 0x0f, + maxSize: 1024, + expectError: "magic number mismatch", + }, + { + name: "wrong header length", + data: []byte{0x0f, 0x00, 0x0b, 0x00}, // wrong header length + magicNumber: 0x0f, + maxSize: 1024, + expectError: "header length mismatch", + }, + { + name: "incomplete header", + data: []byte{0x0f, 0x00, 0x0a, 0x00, 0x01, 0x00}, // incomplete header + magicNumber: 0x0f, + maxSize: 1024, + expectError: "failed to read header", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reader := bytes.NewReader(tt.data) + + err := LengthPrefixedChunking(reader, tt.magicNumber, tt.maxSize, func(data []byte) error { + return nil + }) + + if err == nil { + t.Errorf("expected error containing %q but got none", tt.expectError) + return + } + + if err.Error() == "" { + t.Errorf("expected error containing %q but got empty error", tt.expectError) + } + }) + } +} + +func TestLengthPrefixedChunking_DataTooLarge(t *testing.T) { + var buf bytes.Buffer + + // Create a chunk that exceeds maxChunkSize + largeDataSize := uint32(100) + maxChunkSize := uint32(50) + + // Write magic number and reserved + buf.WriteByte(0x0f) + buf.WriteByte(0x00) + // Write header length + buf.Write([]byte{0x0a, 0x00}) + + // Create header with large data size + header := make([]byte, 10) + binary.LittleEndian.PutUint32(header[:4], largeDataSize) + + buf.Write(header) + + reader := bytes.NewReader(buf.Bytes()) + + err := LengthPrefixedChunking(reader, 0x0f, maxChunkSize, func(data []byte) error { + return nil + }) + + if err == nil { + t.Error("expected error for data too large but got none") + return + } + + if err.Error() == "" { + t.Error("expected error message but got empty") + } +} diff --git a/internal/utils/routine/pool.go b/internal/utils/routine/pool.go index 2556d36e30..7219f31ff0 100644 --- a/internal/utils/routine/pool.go +++ b/internal/utils/routine/pool.go @@ -9,7 +9,7 @@ import ( "github.com/getsentry/sentry-go" "github.com/langgenius/dify-plugin-daemon/internal/utils/log" - "github.com/panjf2000/ants" + "github.com/panjf2000/ants/v2" ) var ( diff --git a/internal/utils/stream/stream_test.go b/internal/utils/stream/stream_test.go index 868e960cd4..56fc136198 100644 --- a/internal/utils/stream/stream_test.go +++ b/internal/utils/stream/stream_test.go @@ -141,6 +141,8 @@ func TestStreamCloseBlockingWrite(t *testing.T) { close(done) }() + // wait for the blocking write to happen + time.Sleep(1 * time.Second) response.Close() select { diff --git a/pkg/entities/dynamic_select_entities/dynamic_select.go b/pkg/entities/dynamic_select_entities/dynamic_select.go new file mode 100644 index 0000000000..3bfd48ef9c --- /dev/null +++ b/pkg/entities/dynamic_select_entities/dynamic_select.go @@ -0,0 +1,7 @@ +package dynamic_select_entities + +import "github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities" + +type DynamicSelectResult struct { + Options []plugin_entities.ParameterOption `json:"options" validate:"omitempty,dive"` +} diff --git a/pkg/entities/model_entities/llm.go b/pkg/entities/model_entities/llm.go index d01275c120..3df6d7dec0 100644 --- a/pkg/entities/model_entities/llm.go +++ b/pkg/entities/model_entities/llm.go @@ -188,6 +188,22 @@ type LLMResultChunk struct { Delta LLMResultChunkDelta `json:"delta" validate:"required"` } +type LLMStructuredOutput struct { + StructuredOutput map[string]any `json:"structured_output" validate:"omitempty"` +} + +type LLMResultChunkWithStructuredOutput struct { + // You might argue that why not embed LLMResultChunk directly? + // `LLMResultChunk` has implemented interface `MarshalJSON`, due to Golang's type embedding, + // it also effectively implements the `MarshalJSON` method of `LLMResultChunkWithStructuredOutput`, + // resulting in a unexpected JSON marshaling of `LLMResultChunkWithStructuredOutput` + Model LLMModel `json:"model" validate:"required"` + SystemFingerprint string `json:"system_fingerprint" validate:"omitempty"` + Delta LLMResultChunkDelta `json:"delta" validate:"required"` + + LLMStructuredOutput +} + /* This is a compatibility layer for the old LLMResultChunk format. The old one has the `PromptMessages` field, we need to ensure the new one is backward compatible. diff --git a/pkg/entities/oauth_entities/oauth.go b/pkg/entities/oauth_entities/oauth.go index 279907ab45..b4be27a2ce 100644 --- a/pkg/entities/oauth_entities/oauth.go +++ b/pkg/entities/oauth_entities/oauth.go @@ -5,5 +5,12 @@ type OAuthGetAuthorizationURLResult struct { } type OAuthGetCredentialsResult struct { + Metadata map[string]any `json:"metadata,omitempty"` Credentials map[string]any `json:"credentials"` + ExpiresAt int64 `json:"expires_at"` +} + +type OAuthRefreshCredentialsResult struct { + Credentials map[string]any `json:"credentials"` + ExpiresAt int64 `json:"expires_at"` } diff --git a/pkg/entities/plugin_entities/agent_declaration.go b/pkg/entities/plugin_entities/agent_declaration.go index 51e8ae8e98..22d6caf909 100644 --- a/pkg/entities/plugin_entities/agent_declaration.go +++ b/pkg/entities/plugin_entities/agent_declaration.go @@ -70,7 +70,7 @@ type AgentStrategyParameter struct { Min *float64 `json:"min" yaml:"min" validate:"omitempty"` Max *float64 `json:"max" yaml:"max" validate:"omitempty"` Precision *int `json:"precision" yaml:"precision" validate:"omitempty"` - Options []ToolParameterOption `json:"options" yaml:"options" validate:"omitempty,dive"` + Options []ParameterOption `json:"options" yaml:"options" validate:"omitempty,dive"` } type AgentStrategyOutputSchema map[string]any diff --git a/pkg/entities/plugin_entities/basic_type.go b/pkg/entities/plugin_entities/basic_type.go index b933ee8795..75da97f616 100644 --- a/pkg/entities/plugin_entities/basic_type.go +++ b/pkg/entities/plugin_entities/basic_type.go @@ -35,6 +35,8 @@ func isBasicType(fl validator.FieldLevel) bool { if fl.Field().IsNil() { return true } + default: + return false } return false diff --git a/pkg/entities/plugin_entities/constant.go b/pkg/entities/plugin_entities/constant.go index 0c759587c4..d977d39fc3 100644 --- a/pkg/entities/plugin_entities/constant.go +++ b/pkg/entities/plugin_entities/constant.go @@ -14,4 +14,12 @@ const ( // TOOL_SELECTOR = "tool-selector" TOOLS_SELECTOR = "array[tools]" ANY = "any" + // DynamicSelect + DYNAMIC_SELECT = "dynamic-select" ) + +type ParameterOption struct { + Value string `json:"value" yaml:"value" validate:"required"` + Label I18nObject `json:"label" yaml:"label" validate:"required"` + Icon string `json:"icon" yaml:"icon" validate:"omitempty"` +} diff --git a/pkg/entities/plugin_entities/model_declaration.go b/pkg/entities/plugin_entities/model_declaration.go index 7592ccd05d..0d56ee24b0 100644 --- a/pkg/entities/plugin_entities/model_declaration.go +++ b/pkg/entities/plugin_entities/model_declaration.go @@ -628,6 +628,8 @@ type ModelProviderDeclaration struct { Description *I18nObject `json:"description" yaml:"description,omitempty" validate:"omitempty"` IconSmall *I18nObject `json:"icon_small" yaml:"icon_small,omitempty" validate:"omitempty"` IconLarge *I18nObject `json:"icon_large" yaml:"icon_large,omitempty" validate:"omitempty"` + IconSmallDark *I18nObject `json:"icon_small_dark" yaml:"icon_small_dark,omitempty" validate:"omitempty"` + IconLargeDark *I18nObject `json:"icon_large_dark" yaml:"icon_large_dark,omitempty" validate:"omitempty"` Background *string `json:"background" yaml:"background,omitempty" validate:"omitempty"` Help *ModelProviderHelpEntity `json:"help" yaml:"help,omitempty" validate:"omitempty"` SupportedModelTypes []ModelType `json:"supported_model_types" yaml:"supported_model_types" validate:"required,lte=16,dive,model_type"` diff --git a/pkg/entities/plugin_entities/plugin_declaration.go b/pkg/entities/plugin_entities/plugin_declaration.go index a95c0128ec..560b5493d6 100644 --- a/pkg/entities/plugin_entities/plugin_declaration.go +++ b/pkg/entities/plugin_entities/plugin_declaration.go @@ -147,6 +147,7 @@ type PluginDeclarationWithoutAdvancedFields struct { Label I18nObject `json:"label" yaml:"label" validate:"required"` Description I18nObject `json:"description" yaml:"description" validate:"required"` Icon string `json:"icon" yaml:"icon,omitempty" validate:"required,max=128"` + IconDark string `json:"icon_dark" yaml:"icon_dark,omitempty" validate:"omitempty,max=128"` Resource PluginResourceRequirement `json:"resource" yaml:"resource,omitempty" validate:"required"` Plugins PluginExtensions `json:"plugins" yaml:"plugins,omitempty" validate:"required"` Meta PluginMeta `json:"meta" yaml:"meta,omitempty" validate:"required"` diff --git a/pkg/entities/plugin_entities/request.go b/pkg/entities/plugin_entities/request.go index e0d3a3918f..969ead6088 100644 --- a/pkg/entities/plugin_entities/request.go +++ b/pkg/entities/plugin_entities/request.go @@ -18,6 +18,7 @@ type InvokePluginRequest[T any] struct { MessageID *string `json:"message_id"` AppID *string `json:"app_id"` EndpointID *string `json:"endpoint_id"` + Context map[string]any `json:"context"` Data T `json:"data" validate:"required"` } diff --git a/pkg/entities/plugin_entities/tool_declaration.go b/pkg/entities/plugin_entities/tool_declaration.go index 39a02d7ddf..5b11815312 100644 --- a/pkg/entities/plugin_entities/tool_declaration.go +++ b/pkg/entities/plugin_entities/tool_declaration.go @@ -33,11 +33,6 @@ func init() { validators.GlobalEntitiesValidator.RegisterValidation("tool_identity_name", isToolIdentityName) } -type ToolParameterOption struct { - Value string `json:"value" yaml:"value" validate:"required"` - Label I18nObject `json:"label" yaml:"label" validate:"required"` -} - type ToolParameterType string const ( @@ -51,7 +46,8 @@ const ( TOOL_PARAMETER_TYPE_APP_SELECTOR ToolParameterType = APP_SELECTOR TOOL_PARAMETER_TYPE_MODEL_SELECTOR ToolParameterType = MODEL_SELECTOR // TOOL_PARAMETER_TYPE_TOOL_SELECTOR ToolParameterType = TOOL_SELECTOR - TOOL_PARAMETER_TYPE_ANY ToolParameterType = ANY + TOOL_PARAMETER_TYPE_ANY ToolParameterType = ANY + TOOL_PARAMETER_TYPE_DYNAMIC_SELECT ToolParameterType = DYNAMIC_SELECT ) func isToolParameterType(fl validator.FieldLevel) bool { @@ -67,7 +63,8 @@ func isToolParameterType(fl validator.FieldLevel) bool { // string(TOOL_PARAMETER_TYPE_TOOL_SELECTOR), string(TOOL_PARAMETER_TYPE_APP_SELECTOR), string(TOOL_PARAMETER_TYPE_MODEL_SELECTOR), - string(TOOL_PARAMETER_TYPE_ANY): + string(TOOL_PARAMETER_TYPE_ANY), + string(TOOL_PARAMETER_TYPE_DYNAMIC_SELECT): return true } return false @@ -134,7 +131,7 @@ type ToolParameter struct { Min *float64 `json:"min" yaml:"min" validate:"omitempty"` Max *float64 `json:"max" yaml:"max" validate:"omitempty"` Precision *int `json:"precision" yaml:"precision" validate:"omitempty"` - Options []ToolParameterOption `json:"options" yaml:"options" validate:"omitempty,dive"` + Options []ParameterOption `json:"options" yaml:"options" validate:"omitempty,dive"` } type ToolDescription struct { @@ -205,6 +202,7 @@ type ToolProviderIdentity struct { Name string `json:"name" validate:"required,tool_provider_identity_name"` Description I18nObject `json:"description"` Icon string `json:"icon" validate:"required"` + IconDark string `json:"icon_dark" validate:"omitempty"` Label I18nObject `json:"label" validate:"required"` Tags []manifest_entities.PluginTag `json:"tags" validate:"omitempty,dive,plugin_tag"` } @@ -223,7 +221,7 @@ func init() { type ToolProviderDeclaration struct { Identity ToolProviderIdentity `json:"identity" yaml:"identity" validate:"required"` CredentialsSchema []ProviderConfig `json:"credentials_schema" yaml:"credentials_schema" validate:"omitempty,dive"` - OAuthSchema *OAuthSchema `json:"oauth_schema" yaml:"oauth_schema" validate:"omitempty,dive"` + OAuthSchema *OAuthSchema `json:"oauth_schema" yaml:"oauth_schema" validate:"omitempty"` Tools []ToolDeclaration `json:"tools" yaml:"tools" validate:"required,dive"` ToolFiles []string `json:"-" yaml:"-"` } diff --git a/pkg/entities/requests/dynamic_select.go b/pkg/entities/requests/dynamic_select.go new file mode 100644 index 0000000000..767fff5428 --- /dev/null +++ b/pkg/entities/requests/dynamic_select.go @@ -0,0 +1,8 @@ +package requests + +type RequestDynamicParameterSelect struct { + Credentials map[string]any `json:"credentials" validate:"required"` + Provider string `json:"provider" validate:"required"` + ProviderAction string `json:"provider_action" validate:"required"` + Parameter string `json:"parameter" validate:"required"` +} diff --git a/pkg/entities/requests/model.go b/pkg/entities/requests/model.go index 63c9f96be6..a0a4b5818c 100644 --- a/pkg/entities/requests/model.go +++ b/pkg/entities/requests/model.go @@ -7,7 +7,8 @@ import ( ) type Credentials struct { - Credentials map[string]any `json:"credentials" validate:"omitempty"` + Credentials map[string]any `json:"credentials" validate:"omitempty"` + CredentialType string `json:"credential_type" validate:"omitempty"` } type BaseRequestInvokeModel struct { diff --git a/pkg/entities/requests/oauth.go b/pkg/entities/requests/oauth.go index fa2b6d64f6..376fac1c95 100644 --- a/pkg/entities/requests/oauth.go +++ b/pkg/entities/requests/oauth.go @@ -2,11 +2,20 @@ package requests type RequestOAuthGetAuthorizationURL struct { Provider string `json:"provider" validate:"required"` + RedirectURI string `json:"redirect_uri" validate:"required"` SystemCredentials map[string]any `json:"system_credentials" validate:"omitempty"` } type RequestOAuthGetCredentials struct { Provider string `json:"provider" validate:"required"` + RedirectURI string `json:"redirect_uri" validate:"required"` SystemCredentials map[string]any `json:"system_credentials" validate:"omitempty"` RawHttpRequest string `json:"raw_http_request" validate:"required"` // hex encoded raw http request from the oauth provider } + +type RequestOAuthRefreshCredentials struct { + Provider string `json:"provider" validate:"required"` + RedirectURI string `json:"redirect_uri" validate:"required"` + SystemCredentials map[string]any `json:"system_credentials" validate:"omitempty"` + Credentials map[string]any `json:"credentials" validate:"required"` +} diff --git a/pkg/entities/requests/tool.go b/pkg/entities/requests/tool.go index 74aeaa70de..1036d2d848 100644 --- a/pkg/entities/requests/tool.go +++ b/pkg/entities/requests/tool.go @@ -11,12 +11,13 @@ const ( TOOL_TYPE_BUILTIN ToolType = "builtin" TOOL_TYPE_WORKFLOW ToolType = "workflow" TOOL_TYPE_API ToolType = "api" + TOOL_TYPE_MCP ToolType = "mcp" ) func init() { validators.GlobalEntitiesValidator.RegisterValidation("tool_type", func(fl validator.FieldLevel) bool { switch fl.Field().String() { - case string(TOOL_TYPE_BUILTIN), string(TOOL_TYPE_WORKFLOW), string(TOOL_TYPE_API): + case string(TOOL_TYPE_BUILTIN), string(TOOL_TYPE_WORKFLOW), string(TOOL_TYPE_API), string(TOOL_TYPE_MCP): return true } return false diff --git a/pkg/plugin_packager/consts/verification.go b/pkg/plugin_packager/consts/verification.go new file mode 100644 index 0000000000..550240d2f6 --- /dev/null +++ b/pkg/plugin_packager/consts/verification.go @@ -0,0 +1,5 @@ +package consts + +const ( + VERIFICATION_FILE = ".verification.dify.json" +) diff --git a/pkg/plugin_packager/decoder/decoder.go b/pkg/plugin_packager/decoder/decoder.go index 804843fe4f..e8fdc26ed0 100644 --- a/pkg/plugin_packager/decoder/decoder.go +++ b/pkg/plugin_packager/decoder/decoder.go @@ -40,6 +40,13 @@ type PluginDecoder interface { // Signature returns the signature of the plugin, if available Signature() (string, error) + // Verified returns true if the plugin is verified + Verified() bool + + // Verification returns the verification of the plugin, if available + // Error will only returns if the plugin is not verified + Verification() (*Verification, error) + // CreateTime returns the creation time of the plugin as a Unix timestamp CreateTime() (int64, error) @@ -57,4 +64,8 @@ type PluginDecoder interface { // Check Assets valid CheckAssetsValid() error + + // AvailableI18nReadme returns a map of available readme i18n, the key is the language, the value is the readme. + // The language code is in the format of IETF BCP 47 language tag + AvailableI18nReadme() (map[string]string, error) } diff --git a/pkg/plugin_packager/decoder/entities.go b/pkg/plugin_packager/decoder/entities.go new file mode 100644 index 0000000000..740cf588e9 --- /dev/null +++ b/pkg/plugin_packager/decoder/entities.go @@ -0,0 +1,19 @@ +package decoder + +type AuthorizedCategory string + +const ( + AUTHORIZED_CATEGORY_LANGGENIUS AuthorizedCategory = "langgenius" + AUTHORIZED_CATEGORY_PARTNER AuthorizedCategory = "partner" + AUTHORIZED_CATEGORY_COMMUNITY AuthorizedCategory = "community" +) + +type Verification struct { + AuthorizedCategory AuthorizedCategory `json:"authorized_category"` +} + +func DefaultVerification() *Verification { + return &Verification{ + AuthorizedCategory: AUTHORIZED_CATEGORY_LANGGENIUS, + } +} diff --git a/pkg/plugin_packager/decoder/fs.go b/pkg/plugin_packager/decoder/fs.go index c195800a7d..e4c32cb13c 100644 --- a/pkg/plugin_packager/decoder/fs.go +++ b/pkg/plugin_packager/decoder/fs.go @@ -169,6 +169,10 @@ func (d *FSPluginDecoder) CreateTime() (int64, error) { return 0, nil } +func (d *FSPluginDecoder) Verification() (*Verification, error) { + return nil, nil +} + func (d *FSPluginDecoder) Manifest() (plugin_entities.PluginDeclaration, error) { return d.PluginDecoderHelper.Manifest(d) } @@ -189,3 +193,11 @@ func (d *FSPluginDecoder) UniqueIdentity() (plugin_entities.PluginUniqueIdentifi func (d *FSPluginDecoder) CheckAssetsValid() error { return d.PluginDecoderHelper.CheckAssetsValid(d) } + +func (d *FSPluginDecoder) Verified() bool { + return d.PluginDecoderHelper.verified(d) +} + +func (d *FSPluginDecoder) AvailableI18nReadme() (map[string]string, error) { + return d.PluginDecoderHelper.AvailableI18nReadme(d, string(filepath.Separator)) +} diff --git a/pkg/plugin_packager/decoder/helper.go b/pkg/plugin_packager/decoder/helper.go index 2553d03d95..cc01cf153f 100644 --- a/pkg/plugin_packager/decoder/helper.go +++ b/pkg/plugin_packager/decoder/helper.go @@ -3,7 +3,9 @@ package decoder import ( "errors" "fmt" + "os" "path/filepath" + "regexp" "strings" "github.com/langgenius/dify-plugin-daemon/internal/utils/parser" @@ -13,6 +15,8 @@ import ( type PluginDecoderHelper struct { pluginDeclaration *plugin_entities.PluginDeclaration checksum string + + verifiedFlag *bool // used to store the verified flag, avoid calling verified function multiple times } func (p *PluginDecoderHelper) Manifest(decoder PluginDecoder) (plugin_entities.PluginDeclaration, error) { @@ -271,22 +275,7 @@ func (p *PluginDecoderHelper) Manifest(decoder PluginDecoder) (plugin_entities.P dec.FillInDefaultValues() - // verify signature - // for ZipPluginDecoder, use the third party signature verification if it is enabled - if zipDecoder, ok := decoder.(*ZipPluginDecoder); ok { - config := zipDecoder.thirdPartySignatureVerificationConfig - if config != nil && config.Enabled && len(config.PublicKeyPaths) > 0 { - dec.Verified = VerifyPluginWithPublicKeyPaths(decoder, config.PublicKeyPaths) == nil - } else { - dec.Verified = VerifyPlugin(decoder) == nil - } - } else { - dec.Verified = VerifyPlugin(decoder) == nil - } - - if err := dec.ManifestValidate(); err != nil { - return plugin_entities.PluginDeclaration{}, err - } + dec.Verified = p.verified(decoder) p.pluginDeclaration = &dec return dec, nil @@ -421,5 +410,79 @@ func (p *PluginDecoderHelper) CheckAssetsValid(decoder PluginDecoder) error { } } + if declaration.IconDark != "" { + if _, ok := assets[declaration.IconDark]; !ok { + return errors.Join(err, fmt.Errorf("plugin dark icon not found")) + } + } + return nil } + +func (p *PluginDecoderHelper) verified(decoder PluginDecoder) bool { + if p.verifiedFlag != nil { + return *p.verifiedFlag + } + + // verify signature + // for ZipPluginDecoder, use the third party signature verification if it is enabled + if zipDecoder, ok := decoder.(*ZipPluginDecoder); ok { + config := zipDecoder.thirdPartySignatureVerificationConfig + if config != nil && config.Enabled && len(config.PublicKeyPaths) > 0 { + verified := VerifyPluginWithPublicKeyPaths(decoder, config.PublicKeyPaths) == nil + p.verifiedFlag = &verified + return verified + } else { + verified := VerifyPlugin(decoder) == nil + p.verifiedFlag = &verified + return verified + } + } else { + verified := VerifyPlugin(decoder) == nil + p.verifiedFlag = &verified + return verified + } +} + +var ( + readmeRegexp = regexp.MustCompile(`^README_([a-z]{2}_[A-Za-z]{2,})\.md$`) +) + +// Only the en_US readme should be at the root as README.md; +// all other readmes should be placed in the readme folder and named in the format README_$language_code.md. +// The separator is the separator of the file path, it's "/" for zip plugin and os.Separator for fs plugin. +func (p *PluginDecoderHelper) AvailableI18nReadme(decoder PluginDecoder, separator string) (map[string]string, error) { + readmes := make(map[string]string) + // read the en_US readme + enUSReadme, err := decoder.ReadFile("README.md") + if err != nil { + // this file must exist or it's not a valid plugin + return nil, errors.Join(err, fmt.Errorf("en_US readme not found")) + } + readmes["en_US"] = string(enUSReadme) + + readmeFiles, err := decoder.ReadDir("readme") + if errors.Is(err, os.ErrNotExist) { + return readmes, nil + } else if err != nil { + return nil, errors.Join(err, fmt.Errorf("an unexpected error occurred while reading readme folder")) + } + + for _, file := range readmeFiles { + // trim the readme folder prefix + file, _ = strings.CutPrefix(file, "readme"+separator) + // using regexp to match the file name + match := readmeRegexp.FindStringSubmatch(file) + if len(match) == 0 { + continue + } + language := match[1] + readme, err := decoder.ReadFile(filepath.Join("readme", file)) + if err != nil { + return nil, errors.Join(err, fmt.Errorf("failed to read readme file: %s", file)) + } + readmes[language] = string(readme) + } + + return readmes, nil +} diff --git a/pkg/plugin_packager/decoder/helper_test.go b/pkg/plugin_packager/decoder/helper_test.go index 9a9c9fa5c7..6519b2ed04 100644 --- a/pkg/plugin_packager/decoder/helper_test.go +++ b/pkg/plugin_packager/decoder/helper_test.go @@ -17,10 +17,17 @@ func (d *UnixPluginDecoder) ReadFile(filename string) ([]byte, error) { } func (d *UnixPluginDecoder) ReadDir(dirname string) ([]string, error) { - return []string{ - "_assets/test.txt", - "_assets/test2.txt", - }, nil + if dirname == "_assets" { + return []string{ + "_assets/test.txt", + "_assets/test2.txt", + }, nil + } else if dirname == "readme" { + return []string{ + "readme/README_zh_Hans.md", + }, nil + } + return nil, nil } func (d *UnixPluginDecoder) Close() error { @@ -47,6 +54,10 @@ func (d *UnixPluginDecoder) UniqueIdentity() (plugin_entities.PluginUniqueIdenti return plugin_entities.PluginUniqueIdentifier(""), nil } +func (d *UnixPluginDecoder) AvailableI18nReadme() (map[string]string, error) { + return d.PluginDecoderHelper.AvailableI18nReadme(d, "/") +} + type WindowsPluginDecoder struct { UnixPluginDecoder } @@ -79,3 +90,13 @@ func TestRemapAssets(t *testing.T) { assert.Equal(t, remappedAssets["test.txt"], []byte("test")) assert.Equal(t, remappedAssets["test2.txt"], []byte("test")) } + +func TestAvailableI18nReadme(t *testing.T) { + decoder := UnixPluginDecoder{} + readmes, err := decoder.AvailableI18nReadme() + if err != nil { + t.Fatalf("Failed to get available i18n readme: %v", err) + } + assert.Equal(t, readmes["en_US"], "test") + assert.Equal(t, readmes["zh_Hans"], "test") +} diff --git a/pkg/plugin_packager/decoder/zip.go b/pkg/plugin_packager/decoder/zip.go index f2774af568..09cbcc0723 100644 --- a/pkg/plugin_packager/decoder/zip.go +++ b/pkg/plugin_packager/decoder/zip.go @@ -15,6 +15,7 @@ import ( "github.com/langgenius/dify-plugin-daemon/internal/utils/parser" "github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities" + "github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/consts" ) type ZipPluginDecoder struct { @@ -24,8 +25,9 @@ type ZipPluginDecoder struct { reader *zip.Reader err error - sig string - createTime int64 + sig string + createTime int64 + verification *Verification thirdPartySignatureVerificationConfig *ThirdPartySignatureVerificationConfig } @@ -35,7 +37,10 @@ type ThirdPartySignatureVerificationConfig struct { PublicKeyPaths []string } -func newZipPluginDecoder(binary []byte, thirdPartySignatureVerificationConfig *ThirdPartySignatureVerificationConfig) (*ZipPluginDecoder, error) { +func newZipPluginDecoder( + binary []byte, + thirdPartySignatureVerificationConfig *ThirdPartySignatureVerificationConfig, +) (*ZipPluginDecoder, error) { reader, err := zip.NewReader(bytes.NewReader(binary), int64(len(binary))) if err != nil { return nil, errors.New(strings.ReplaceAll(err.Error(), "zip", "difypkg")) @@ -66,7 +71,10 @@ func NewZipPluginDecoder(binary []byte) (*ZipPluginDecoder, error) { // NewZipPluginDecoderWithThirdPartySignatureVerificationConfig is a helper function // to create a ZipPluginDecoder with a third party signature verification -func NewZipPluginDecoderWithThirdPartySignatureVerificationConfig(binary []byte, thirdPartySignatureVerificationConfig *ThirdPartySignatureVerificationConfig) (*ZipPluginDecoder, error) { +func NewZipPluginDecoderWithThirdPartySignatureVerificationConfig( + binary []byte, + thirdPartySignatureVerificationConfig *ThirdPartySignatureVerificationConfig, +) (*ZipPluginDecoder, error) { return newZipPluginDecoder(binary, thirdPartySignatureVerificationConfig) } @@ -180,6 +188,7 @@ func (z *ZipPluginDecoder) decode() error { Signature string `json:"signature"` Time int64 `json:"time"` }](z.reader.Comment) + if err != nil { return err } @@ -187,8 +196,30 @@ func (z *ZipPluginDecoder) decode() error { pluginSig := signatureData.Signature pluginTime := signatureData.Time + var verification *Verification + + // try to read the verification file + verificationData, err := z.ReadFile(consts.VERIFICATION_FILE) + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + return err + } + + // if the verification file is not found, set the verification to nil + verification = nil + } else { + // unmarshal the verification data + verificationData, err := parser.UnmarshalJsonBytes[Verification](verificationData) + if err != nil { + return err + } + + verification = &verificationData + } + z.sig = pluginSig z.createTime = pluginTime + z.verification = verification return nil } @@ -227,6 +258,25 @@ func (z *ZipPluginDecoder) CreateTime() (int64, error) { return z.createTime, nil } +func (z *ZipPluginDecoder) Verification() (*Verification, error) { + if !z.Verified() { + return nil, errors.New("plugin is not verified") + } + + if z.verification != nil { + return z.verification, nil + } + + err := z.decode() + if err != nil { + return nil, err + } + + // if the plugin is verified but the verification is nil + // it's historical reason that all plugins are signed by langgenius + return nil, nil +} + func (z *ZipPluginDecoder) Manifest() (plugin_entities.PluginDeclaration, error) { return z.PluginDecoderHelper.Manifest(z) } @@ -279,3 +329,11 @@ func (z *ZipPluginDecoder) ExtractTo(dst string) error { func (z *ZipPluginDecoder) CheckAssetsValid() error { return z.PluginDecoderHelper.CheckAssetsValid(z) } + +func (z *ZipPluginDecoder) Verified() bool { + return z.PluginDecoderHelper.verified(z) +} + +func (z *ZipPluginDecoder) AvailableI18nReadme() (map[string]string, error) { + return z.PluginDecoderHelper.AvailableI18nReadme(z, "/") +} diff --git a/pkg/plugin_packager/packager/packager.go b/pkg/plugin_packager/packager/packager.go index 10837f48aa..ef010cebbe 100644 --- a/pkg/plugin_packager/packager/packager.go +++ b/pkg/plugin_packager/packager/packager.go @@ -4,8 +4,9 @@ import ( "archive/zip" "bytes" "errors" + "fmt" "path/filepath" - "strconv" + "sort" "strings" "github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/decoder" @@ -34,16 +35,32 @@ func (p *Packager) Pack(maxSize int64) ([]byte, error) { totalSize := int64(0) + var files []FileInfoWithPath + err = p.decoder.Walk(func(filename, dir string) error { fullPath := filepath.Join(dir, filename) file, err := p.decoder.ReadFile(fullPath) if err != nil { return err } - - totalSize += int64(len(file)) + fileSize := int64(len(file)) + files = append(files, FileInfoWithPath{Path: fullPath, Size: fileSize}) + totalSize += fileSize if totalSize > maxSize { - return errors.New("plugin package size is too large, please ensure the uncompressed size is less than " + strconv.FormatInt(maxSize, 10) + " bytes") + sort.Slice(files, func(i, j int) bool { + return files[i].Size > files[j].Size + }) + fileTop5Info := "" + top := 5 + if len(files) < 5 { + top = len(files) + } + for i := 0; i < top; i++ { + fileTop5Info += fmt.Sprintf("%d. name: %s, size: %d bytes\n", i+1, files[i].Path, files[i].Size) + } + errMsg := fmt.Sprintf("Plugin package size is too large. Please ensure the uncompressed size is less than %d bytes.\nPackaged file info:\n%s", + maxSize, fileTop5Info) + return errors.New(errMsg) } // ISSUES: Windows path separator is \, but zip requires /, to avoid this we just simply replace all \ with / for now @@ -74,3 +91,8 @@ func (p *Packager) Pack(maxSize int64) ([]byte, error) { return zipBuffer.Bytes(), nil } + +type FileInfoWithPath struct { + Path string + Size int64 +} diff --git a/pkg/plugin_packager/packager_test.go b/pkg/plugin_packager/packager_test.go index 56b072104c..48ab4ae01c 100644 --- a/pkg/plugin_packager/packager_test.go +++ b/pkg/plugin_packager/packager_test.go @@ -173,7 +173,9 @@ func TestPackagerAndVerifier(t *testing.T) { } // sign - signed, err := signer.SignPlugin(zip) + signed, err := signer.SignPlugin(zip, &decoder.Verification{ + AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_LANGGENIUS, + }) if err != nil { t.Errorf("failed to sign: %s", err.Error()) return @@ -213,7 +215,9 @@ func TestWrongSign(t *testing.T) { } // sign - signed, err := signer.SignPlugin(zip) + signed, err := signer.SignPlugin(zip, &decoder.Verification{ + AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_LANGGENIUS, + }) if err != nil { t.Errorf("failed to sign: %s", err.Error()) return @@ -293,7 +297,9 @@ func TestSignPluginWithPrivateKey(t *testing.T) { } // sign with private key 1 and create decoder - signed1, err := withkey.SignPluginWithPrivateKey(zip, privateKey1) + signed1, err := withkey.SignPluginWithPrivateKey(zip, &decoder.Verification{ + AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_LANGGENIUS, + }, privateKey1) if err != nil { t.Errorf("failed to sign with private key 1: %s", err.Error()) return @@ -305,7 +311,9 @@ func TestSignPluginWithPrivateKey(t *testing.T) { } // sign with private key 2 and create decoder - signed2, err := withkey.SignPluginWithPrivateKey(zip, privateKey2) + signed2, err := withkey.SignPluginWithPrivateKey(zip, &decoder.Verification{ + AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_LANGGENIUS, + }, privateKey2) if err != nil { t.Errorf("failed to sign with private key 2: %s", err.Error()) return @@ -416,7 +424,9 @@ func TestVerifyPluginWithThirdPartyKeys(t *testing.T) { } // sign with private key 1 and create decoder - signed1, err := withkey.SignPluginWithPrivateKey(zip, privateKey1) + signed1, err := withkey.SignPluginWithPrivateKey(zip, &decoder.Verification{ + AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_LANGGENIUS, + }, privateKey1) if err != nil { t.Errorf("failed to sign with private key 1: %s", err.Error()) return @@ -428,7 +438,9 @@ func TestVerifyPluginWithThirdPartyKeys(t *testing.T) { } // sign with private key 2 and create decoder - signed2, err := withkey.SignPluginWithPrivateKey(zip, privateKey2) + signed2, err := withkey.SignPluginWithPrivateKey(zip, &decoder.Verification{ + AuthorizedCategory: decoder.AUTHORIZED_CATEGORY_LANGGENIUS, + }, privateKey2) if err != nil { t.Errorf("failed to sign with private key 2: %s", err.Error()) return diff --git a/pkg/plugin_packager/signer/sign.go b/pkg/plugin_packager/signer/sign.go index 1f54a7b8bc..e4fe907e8a 100644 --- a/pkg/plugin_packager/signer/sign.go +++ b/pkg/plugin_packager/signer/sign.go @@ -3,6 +3,7 @@ package signer import ( "github.com/langgenius/dify-plugin-daemon/internal/core/license/private_key" "github.com/langgenius/dify-plugin-daemon/internal/utils/encryption" + "github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/decoder" "github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/signer/withkey" ) @@ -14,12 +15,24 @@ import ( // SignPlugin is a function that signs a plugin // It takes a plugin as a stream of bytes and signs it with RSA-4096 with a bundled private key -func SignPlugin(plugin []byte) ([]byte, error) { +func SignPlugin(plugin []byte, verification *decoder.Verification) ([]byte, error) { // load private key privateKey, err := encryption.LoadPrivateKey(private_key.PRIVATE_KEY) if err != nil { return nil, err } - return withkey.SignPluginWithPrivateKey(plugin, privateKey) + return withkey.SignPluginWithPrivateKey(plugin, verification, privateKey) +} + +// TraditionalSignPlugin, only used for testing +// WARNING: This function is deprecated, use SignPlugin instead +func TraditionalSignPlugin(plugin []byte) ([]byte, error) { + // load private key + privateKey, err := encryption.LoadPrivateKey(private_key.PRIVATE_KEY) + if err != nil { + return nil, err + } + + return withkey.TraditionalSignPlugin(plugin, privateKey) } diff --git a/pkg/plugin_packager/signer/withkey/sign_with_key.go b/pkg/plugin_packager/signer/withkey/sign_with_key.go index 0ce5e48a6a..2fa8b88caf 100644 --- a/pkg/plugin_packager/signer/withkey/sign_with_key.go +++ b/pkg/plugin_packager/signer/withkey/sign_with_key.go @@ -6,6 +6,7 @@ import ( "crypto/rsa" "crypto/sha256" "encoding/base64" + "errors" "io" "path" "strconv" @@ -13,12 +14,133 @@ import ( "github.com/langgenius/dify-plugin-daemon/internal/utils/encryption" "github.com/langgenius/dify-plugin-daemon/internal/utils/parser" + "github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/consts" "github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/decoder" ) // SignPluginWithPrivateKey is a function that signs a plugin // It takes a plugin as a stream of bytes and a private key to sign it with RSA-4096 -func SignPluginWithPrivateKey(plugin []byte, privateKey *rsa.PrivateKey) ([]byte, error) { +func SignPluginWithPrivateKey( + plugin []byte, + verification *decoder.Verification, + privateKey *rsa.PrivateKey, +) ([]byte, error) { + decoder, err := decoder.NewZipPluginDecoder(plugin) + if err != nil { + return nil, err + } + + if verification == nil { + return nil, errors.New("verification cannot be nil") + } + + // create a new zip writer + zipBuffer := new(bytes.Buffer) + zipWriter := zip.NewWriter(zipBuffer) + + defer zipWriter.Close() + // store temporary hash + data := new(bytes.Buffer) + // read one by one + err = decoder.Walk(func(filename, dir string) error { + file, err := decoder.ReadFile(path.Join(dir, filename)) + if err != nil { + return err + } + + // calculate sha256 hash of the file + hash := sha256.New() + hash.Write(file) + hashed := hash.Sum(nil) + + // write the hash into data + data.Write(hashed) + + // create a new file in the zip writer + fileWriter, err := zipWriter.Create(path.Join(dir, filename)) + if err != nil { + return err + } + + _, err = io.Copy(fileWriter, bytes.NewReader(file)) + if err != nil { + return err + } + + return nil + }) + + if err != nil { + return nil, err + } + + // write the verification into data + // NOTE: .verification.dify.json is a special file that contains the verification information + // and it will be placed at the end of the zip file, checksum is calculated using it also + verificationBytes := parser.MarshalJsonBytes(verification) + + // write verification into the zip file + fileWriter, err := zipWriter.Create(consts.VERIFICATION_FILE) + if err != nil { + return nil, err + } + + if _, err := fileWriter.Write(verificationBytes); err != nil { + return nil, err + } + + // hash the verification + hash := sha256.New() + hash.Write(verificationBytes) + hashed := hash.Sum(nil) + + // write the hash into data + if _, err := data.Write(hashed); err != nil { + return nil, err + } + + // get current time + ct := time.Now().Unix() + + // convert time to bytes + timeString := strconv.FormatInt(ct, 10) + + // write the time into data + data.Write([]byte(timeString)) + + // sign the data + signature, err := encryption.RSASign(privateKey, data.Bytes()) + if err != nil { + return nil, err + } + + // write the signature into the comment field of the zip file + comments := parser.MarshalJson(map[string]any{ + "signature": base64.StdEncoding.EncodeToString(signature), + "time": ct, + }) + + // write signature + err = zipWriter.SetComment(comments) + if err != nil { + return nil, err + } + + // close the zip writer + err = zipWriter.Close() + if err != nil { + return nil, err + } + + return zipBuffer.Bytes(), nil +} + +// Only used for testing +// WARNING: This function is deprecated, use SignPluginWithPrivateKey instead +func TraditionalSignPlugin( + plugin []byte, + privateKey *rsa.PrivateKey, +) ([]byte, error) { decoder, err := decoder.NewZipPluginDecoder(plugin) if err != nil { return nil, err