Browse Source

enhance: onboarding

Yeuoly 8 months ago
parent
commit
939b9d34b9

+ 27 - 1
cmd/commandline/init/category.go

@@ -6,6 +6,31 @@ import (
 	tea "github.com/charmbracelet/bubbletea"
 )
 
+const (
+	// Colors
+	RESET = "\033[0m"
+	BOLD  = "\033[1m"
+
+	// Foreground colors
+	GREEN  = "\033[32m"
+	YELLOW = "\033[33m"
+	BLUE   = "\033[34m"
+)
+
+const PLUGIN_GUIDE = `But before starting, you need some basic knowledge about the Plugin types, Plugin supports to extend the following abilities in Dify:
+` + "\n" + BOLD + `- Tool` + RESET + `: ` + GREEN + `Tool Providers like Google Search, Stable Diffusion, etc. it can be used to perform a specific task.` + RESET + `
+` + BOLD + `- Model` + RESET + `: ` + GREEN + `Model Providers like OpenAI, Anthropic, etc. you can use their models to enhance the AI capabilities.` + RESET + `
+` + BOLD + `- Endpoint` + RESET + `: ` + GREEN + `Like Service API in Dify and Ingress in Kubernetes, you can extend a http service as an endpoint and control its logics using your own code.` + RESET + `
+
+Based on the ability you want to extend, we have divided the Plugin into three types: ` + BOLD + `Tool` + RESET + `, ` + BOLD + `Model` + RESET + `, and ` + BOLD + `Extension` + RESET + `.
+
+` + BOLD + `- Tool` + RESET + `: ` + YELLOW + `It's a tool provider, but not only limited to tools, you can implement a endpoint there, for example, you need both ` + BLUE + `Sending Message` + RESET + YELLOW + ` and ` + BLUE + `Receiving Message` + RESET + YELLOW + ` if you are building a Discord Bot, ` + BOLD + `Tool` + RESET + YELLOW + ` and ` + BOLD + `Endpoint` + RESET + YELLOW + ` are both required.` + RESET + `
+` + BOLD + `- Model` + RESET + `: ` + YELLOW + `Just a model provider, extending others is not allowed.` + RESET + `
+` + BOLD + `- Extension` + RESET + `: ` + YELLOW + `Other times, you may only need a simple http service to extend the functionalities, ` + BOLD + `Extension` + RESET + YELLOW + ` is the right choice for you.` + RESET + `
+` + `
+What's more, we have provided the template for you, you can choose one of them below:
+`
+
 type category struct {
 	cursor int
 }
@@ -33,7 +58,8 @@ func (c category) Category() string {
 }
 
 func (c category) View() string {
-	s := "Select the type of plugin you want to create\n"
+	s := "Select the type of plugin you want to create, and press `Enter` to continue\n"
+	s += PLUGIN_GUIDE
 	for i, category := range categories {
 		if i == c.cursor {
 			s += fmt.Sprintf("\033[32m-> %s\033[0m\n", category)

+ 13 - 15
cmd/commandline/init/init.go

@@ -191,28 +191,26 @@ func (m model) createPlugin() {
 	}
 
 	plugin_dir := filepath.Join(cwd, manifest.Name)
-	if err := os.MkdirAll(plugin_dir, 0o755); err != nil {
-		log.Error("failed to create plugin directory: %s", err)
-		return
-	}
 
-	manifest_file_path := filepath.Join(plugin_dir, "manifest.yaml")
-	if err := os.WriteFile(manifest_file_path, manifest_file, 0o644); err != nil {
+	if err := writeFile(filepath.Join(plugin_dir, "manifest.yaml"), string(manifest_file)); err != nil {
 		log.Error("failed to write manifest file: %s", err)
 		return
 	}
 
-	// create _assets directory
-	assets_dir := filepath.Join(plugin_dir, "_assets")
-	if err := os.MkdirAll(assets_dir, 0o755); err != nil {
-		log.Error("failed to create assets directory: %s", err)
+	// create icon.svg
+	if err := writeFile(filepath.Join(plugin_dir, "_assets", "icon.svg"), string(icon)); err != nil {
+		log.Error("failed to write icon file: %s", err)
 		return
 	}
 
-	// create icon.svg
-	icon_file_path := filepath.Join(assets_dir, "icon.svg")
-	if err := os.WriteFile(icon_file_path, icon, 0o644); err != nil {
-		log.Error("failed to write icon file: %s", err)
+	// create README.md
+	readme, err := renderTemplate(README, manifest, []string{})
+	if err != nil {
+		log.Error("failed to render README template: %s", err)
+		return
+	}
+	if err := writeFile(filepath.Join(plugin_dir, "README.md"), readme); err != nil {
+		log.Error("failed to write README file: %s", err)
 		return
 	}
 
@@ -229,5 +227,5 @@ func (m model) createPlugin() {
 
 	success = true
 
-	log.Info("plugin %s created successfully", manifest.Name)
+	log.Info("plugin %s created successfully, you can refer to `%s/GUIDE.md` for more information about how to develop it", manifest.Name, manifest.Name)
 }

+ 3 - 1
cmd/commandline/init/language.go

@@ -28,7 +28,9 @@ func (l language) Language() constants.Language {
 }
 
 func (l language) View() string {
-	s := "Select the language you want to use for plugin development\n"
+	s := `Select the language you want to use for plugin development, and press ` + GREEN + `Enter` + RESET + ` to continue, 
+BTW, you need Python 3.10+ to develop the Plugin if you choose Python.
+`
 	for i, language := range languages {
 		if i == l.cursor {
 			s += fmt.Sprintf("\033[32m-> %s\033[0m\n", language)

+ 12 - 12
cmd/commandline/init/permission.go

@@ -62,29 +62,29 @@ func (p permission) View() string {
 	s := "Configure the permissions of the plugin, use up and down to navigate, enter to select, after selection, press right to move to the next menu\n"
 	s += "Backwards Invocation:\n"
 	s += "Tools:\n"
-	s += fmt.Sprintf("  %sEnabled: %v\n", cursor("tool.enabled"), checked(p.permission.AllowInvokeTool()))
+	s += fmt.Sprintf("  %sEnabled: %v %s You can invoke tools inside Dify if it's enabled %s\n", cursor("tool.enabled"), checked(p.permission.AllowInvokeTool()), YELLOW, RESET)
 	s += "Models:\n"
-	s += fmt.Sprintf("  %sEnabled: %v\n", cursor("model.enabled"), checked(p.permission.AllowInvokeModel()))
-	s += fmt.Sprintf("  %sLLM: %v\n", cursor("model.llm"), checked(p.permission.AllowInvokeLLM()))
-	s += fmt.Sprintf("  %sText Embedding: %v\n", cursor("model.text_embedding"), checked(p.permission.AllowInvokeTextEmbedding()))
-	s += fmt.Sprintf("  %sRerank: %v\n", cursor("model.rerank"), checked(p.permission.AllowInvokeRerank()))
-	s += fmt.Sprintf("  %sTTS: %v\n", cursor("model.tts"), checked(p.permission.AllowInvokeTTS()))
-	s += fmt.Sprintf("  %sSpeech2Text: %v\n", cursor("model.speech2text"), checked(p.permission.AllowInvokeSpeech2Text()))
-	s += fmt.Sprintf("  %sModeration: %v\n", cursor("model.moderation"), checked(p.permission.AllowInvokeModeration()))
+	s += fmt.Sprintf("  %sEnabled: %v %s You can invoke models inside Dify if it's enabled %s\n", cursor("model.enabled"), checked(p.permission.AllowInvokeModel()), YELLOW, RESET)
+	s += fmt.Sprintf("  %sLLM: %v %s You can invoke LLM models inside Dify if it's enabled %s\n", cursor("model.llm"), checked(p.permission.AllowInvokeLLM()), YELLOW, RESET)
+	s += fmt.Sprintf("  %sText Embedding: %v %s You can invoke text embedding models inside Dify if it's enabled %s\n", cursor("model.text_embedding"), checked(p.permission.AllowInvokeTextEmbedding()), YELLOW, RESET)
+	s += fmt.Sprintf("  %sRerank: %v %s You can invoke rerank models inside Dify if it's enabled %s\n", cursor("model.rerank"), checked(p.permission.AllowInvokeRerank()), YELLOW, RESET)
+	s += fmt.Sprintf("  %sTTS: %v %s You can invoke TTS models inside Dify if it's enabled %s\n", cursor("model.tts"), checked(p.permission.AllowInvokeTTS()), YELLOW, RESET)
+	s += fmt.Sprintf("  %sSpeech2Text: %v %s You can invoke speech2text models inside Dify if it's enabled %s\n", cursor("model.speech2text"), checked(p.permission.AllowInvokeSpeech2Text()), YELLOW, RESET)
+	s += fmt.Sprintf("  %sModeration: %v %s You can invoke moderation models inside Dify if it's enabled %s\n", cursor("model.moderation"), checked(p.permission.AllowInvokeModeration()), YELLOW, RESET)
 	s += "Apps:\n"
-	s += fmt.Sprintf("  %sEnabled: %v\n", cursor("app.enabled"), checked(p.permission.AllowInvokeApp()))
+	s += fmt.Sprintf("  %sEnabled: %v %s Ability to invoke apps like BasicChat/ChatFlow/Agent/Workflow etc. %s\n", cursor("app.enabled"), checked(p.permission.AllowInvokeApp()), YELLOW, RESET)
 	s += "Resources:\n"
 	s += "Storage:\n"
-	s += fmt.Sprintf("  %sEnabled: %v\n", cursor("storage.enabled"), checked(p.permission.AllowInvokeStorage()))
+	s += fmt.Sprintf("  %sEnabled: %v %s Persistence storage for the plugin %s\n", cursor("storage.enabled"), checked(p.permission.AllowInvokeStorage()), YELLOW, RESET)
 
 	if p.permission.AllowInvokeStorage() {
 		s += fmt.Sprintf("  %sSize: %v\n", cursor("storage.size"), p.storageSizeEditor.View())
 	} else {
-		s += fmt.Sprintf("  %sSize: %v\n", cursor("storage.size"), "N/A")
+		s += fmt.Sprintf("  %sSize: %v %s The maximum size of the storage %s\n", cursor("storage.size"), "N/A", YELLOW, RESET)
 	}
 
 	s += "Endpoints:\n"
-	s += fmt.Sprintf("  %sEnabled: %v\n", cursor("endpoint.enabled"), checked(p.permission.AllowRegisterEndpoint()))
+	s += fmt.Sprintf("  %sEnabled: %v %s Ability to register endpoints %s\n", cursor("endpoint.enabled"), checked(p.permission.AllowRegisterEndpoint()), YELLOW, RESET)
 	return s
 }
 

+ 14 - 1
cmd/commandline/init/python.go

@@ -82,6 +82,9 @@ var PYTHON_ENDPOINT_TEMPLATE []byte
 //go:embed templates/python/endpoint.yaml
 var PYTHON_ENDPOINT_MANIFEST_TEMPLATE []byte
 
+//go:embed templates/python/GUIDE.md
+var PYTHON_GUIDE []byte
+
 func renderTemplate(
 	original_template []byte, manifest *plugin_entities.PluginDeclaration, supported_model_types []string,
 ) (string, error) {
@@ -98,6 +101,9 @@ func renderTemplate(
 		"Author":              manifest.Author,
 		"PluginDescription":   manifest.Description.EnUS,
 		"SupportedModelTypes": supported_model_types,
+		"Version":             manifest.Version,
+		"Date":                manifest.CreatedAt,
+		"Category":            manifest.Category(),
 	}); err != nil {
 		return "", err
 	}
@@ -115,7 +121,14 @@ func writeFile(path string, content string) error {
 func createPythonEnvironment(
 	root string, entrypoint string, manifest *plugin_entities.PluginDeclaration, category string,
 ) error {
-	// TODO: enhance to use template renderer
+	guide, err := renderTemplate(PYTHON_GUIDE, manifest, []string{})
+	if err != nil {
+		return err
+	}
+
+	if err := writeFile(filepath.Join(root, "GUIDE.md"), guide); err != nil {
+		return err
+	}
 
 	// create the python environment
 	entrypoint_file_path := filepath.Join(root, fmt.Sprintf("%s.py", entrypoint))

+ 8 - 0
cmd/commandline/init/template.go

@@ -0,0 +1,8 @@
+package init
+
+import (
+	_ "embed"
+)
+
+//go:embed templates/README.md
+var README []byte

+ 11 - 0
cmd/commandline/init/templates/README.md

@@ -0,0 +1,11 @@
+## {{ .PluginName }}
+
+**Author:** {{ .Author }}
+**Version:** {{ .Version }}
+**Date:** {{ .Date }}
+**Type:** {{ .Category }}
+
+### Description
+
+{{ .Description }}
+

+ 4 - 0
cmd/commandline/init/templates/python/.env.example

@@ -0,0 +1,4 @@
+INSTALL_METHOD=REMOTE
+REMOTE_INSTALL_HOST=debug-plugin.dify.dev
+REMOTE_INSTALL_PORT=5003
+REMOTE_INSTALL_KEY=********-****-****-****-************

+ 112 - 0
cmd/commandline/init/templates/python/GUIDE.md

@@ -0,0 +1,112 @@
+## User Guide of how to develop a Dify Plugin
+
+Hi there, looks like you have already created a Plugin, now let's get you started with the development!
+
+### Choose a Plugin type you want to develop
+
+Before start, you need some basic knowledge about the Plugin types, Plugin supports to extend the following abilities in Dify:
+- **Tool**: Tool Providers like Google Search, Stable Diffusion, etc. it can be used to perform a specific task.
+- **Model**: Model Providers like OpenAI, Anthropic, etc. you can use their models to enhance the AI capabilities.
+- **Endpoint**: Like Service API in Dify and Ingress in Kubernetes, you can extend a http service as an endpoint and control its logics using your own code.
+
+Based on the ability you want to extend, we have divided the Plugin into three types: **Tool**, **Model**, and **Extension**.
+
+- **Tool**: It's a tool provider, but not only limited to tools, you can implement a endpoint there, for example, you need both `Sending Message` and `Receiving Message` if you are building a Discord Bot, **Tool** and **Endpoint** are both required.
+- **Model**: Just a model provider, extending others is not allowed.
+- **Extension**: Other times, you may only need a simple http service to extend the functionalities, **Extension** is the right choice for you.
+
+I believe you have chosen the right type for your Plugin while creating it, if not, you can change it later by modifying the `manifest.yaml` file.
+
+### Manifest
+
+Now you can edit the `manifest.yaml` file to describe your Plugin, here is the basic structure of it:
+
+- version(version, required):Plugin's version
+- type(type, required):Plugin's type, currently only supports `plugin`, future support `bundle`
+- author(string, required):作者,在 Marketplace 中定义为组织名
+- label(label, required):Multi-language name
+- created_at(RFC3339, required):Creation time, Marketplace requires that the creation time must be less than the current time
+- icon(asset, required):Icon path
+- resource (object):Resources to be applied
+  - memory (int64):Maximum memory usage, mainly related to resource application on SaaS for serverless, unit bytes
+  - permission(object):Permission application
+    - tool(object):Reverse call tool permission
+      - enabled (bool)
+    - model(object):Reverse call model permission
+      - enabled(bool)
+      - llm(bool)
+      - text_embedding(bool)
+      - rerank(bool)
+      - tts(bool)
+      - speech2text(bool)
+      - moderation(bool)
+    - node(object):Reverse call node permission
+      - enabled(bool) 
+    - endpoint(object):Allow to register endpoint permission
+      - enabled(bool)
+    - app(object):Reverse call app permission
+      - enabled(bool)
+    - storage(object):Apply for persistent storage permission
+      - enabled(bool)
+      - size(int64):Maximum allowed persistent memory, unit bytes
+- plugins(object, required):Plugin extension specific ability yaml file list, absolute path in the plugin package, if you need to extend the model, you need to define a file like openai.yaml, and fill in the path here, and the file on the path must exist, otherwise the packaging will fail.
+  - Format
+    - tools(list[string]): Extended tool suppliers, as for the detailed format, please refer to [Tool Guide](https://docs.dify.ai/docs/plugins/standard/tool_provider)
+    - models(list[string]):Extended model suppliers, as for the detailed format, please refer to [Model Guide](https://docs.dify.ai/docs/plugins/standard/model_provider)
+    - endpoints(list[string]):Extended Endpoints suppliers, as for the detailed format, please refer to [Endpoint Guide](https://docs.dify.ai/docs/plugins/standard/endpoint_group)
+  - Restrictions
+    - Not allowed to extend both tools and models
+    - Not allowed to have no extension
+    - Not allowed to extend both models and endpoints
+    - Currently only supports up to one supplier of each type of extension
+- meta(object)
+  - version(version, required):manifest format version, initial version 0.0.1
+  - arch(list[string], required):Supported architectures, currently only supports amd64 arm64
+  - runner(object, required):Runtime configuration
+    - language(string):Currently only supports python
+    - version(string):Language version, currently only supports 3.12
+    - entrypoint(string):Program entry, in python it should be main
+
+### Install Dependencies
+
+- First of all, you need a Python 3.10+ environment, as our SDK requires that.
+- Then, install the dependencies:
+    ```bash
+    pip install -r requirements.txt
+    ```
+- If you want to add more dependencies, you can add them to the `requirements.txt` file, once you have set the runner to python in the `manifest.yaml` file, `requirements.txt` will be automatically generated and used for packaging and deployment.
+
+### Implement the Plugin
+
+Now you can start to implement your Plugin, by following these examples, you can quickly understand how to implement your own Plugin:
+
+- [OpenAI](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/openai): best practice for model provider
+- [Google Search](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/google): a simple example for tool provider
+- [Neko](https://github.com/langgenius/dify-plugin-sdks/tree/main/python/examples/neko): a funny example for endpoint group
+
+### Test and Debug the Plugin
+
+You may already noticed that a `.env.example` file in the root directory of your Plugin, just copy it to `.env` and fill in the corresponding values, there are some environment variables you need to set if you want to debug your Plugin locally.
+
+- `INSTALL_METHOD`: Set this to `remote`, your plugin will connect to a Dify instance through the network.
+- `REMOTE_INSTALL_HOST`: The host of your Dify instance, you can use our SaaS instance `https://debug.dify.ai`, or self-hosted Dify instance.
+- `REMOTE_INSTALL_PORT`: The port of your Dify instance, default is 5003
+- `REMOTE_INSTALL_KEY`: You should get your debugging key from the Dify instance you used, at the right top of the plugin management page, you can see a button with a `debug` icon, click it and you will get the key.
+
+Run the following command to start your Plugin:
+
+```bash
+python -m main
+```
+
+Refresh the page of your Dify instance, you should be able to see your Plugin in the list now, but it will be marked as `debugging`, you can use it normally, but not recommended for production.
+
+### Package the Plugin
+
+After all, just package your Plugin by running the following command:
+
+```bash
+dify-plugin plugin package ./ROOT_DIRECTORY_OF_YOUR_PLUGIN
+```
+
+you will get a `plugin.difypkg` file, that's all, you can submit it to the Marketplace now, look forward to your Plugin being listed!

+ 18 - 0
internal/types/entities/plugin_entities/plugin_declaration.go

@@ -12,6 +12,14 @@ import (
 	"github.com/langgenius/dify-plugin-daemon/internal/utils/parser"
 )
 
+type PluginCategory string
+
+const (
+	PLUGIN_CATEGORY_TOOL      PluginCategory = "tool"
+	PLUGIN_CATEGORY_MODEL     PluginCategory = "model"
+	PLUGIN_CATEGORY_EXTENSION PluginCategory = "extension"
+)
+
 type DifyManifestType string
 
 const (
@@ -224,6 +232,16 @@ type PluginDeclaration struct {
 	Tool                                   *ToolProviderDeclaration     `json:"tool,omitempty" yaml:"tool,omitempty" validate:"omitempty"`
 }
 
+func (p *PluginDeclaration) Category() PluginCategory {
+	if p.Tool != nil {
+		return PLUGIN_CATEGORY_TOOL
+	}
+	if p.Model != nil {
+		return PLUGIN_CATEGORY_MODEL
+	}
+	return PLUGIN_CATEGORY_EXTENSION
+}
+
 func (p *PluginDeclaration) UnmarshalJSON(data []byte) error {
 	// First unmarshal the embedded struct
 	if err := json.Unmarshal(data, &p.PluginDeclarationWithoutAdvancedFields); err != nil {