소스 검색

feat: cli

Yeuoly 11 달 전
부모
커밋
a2aedc7355

+ 20 - 0
cmd/commandline/encoder.go

@@ -0,0 +1,20 @@
+package main
+
+import (
+	"bytes"
+
+	"github.com/langgenius/dify-plugin-daemon/internal/utils/log"
+	"gopkg.in/yaml.v3"
+)
+
+func marshalYamlBytes(v any) []byte {
+	buf := bytes.NewBuffer([]byte{})
+	encoder := yaml.NewEncoder(buf)
+	encoder.SetIndent(2)
+	err := encoder.Encode(v)
+	if err != nil {
+		log.Error("failed to marshal yaml: %s", err)
+		return nil
+	}
+	return buf.Bytes()
+}

+ 157 - 0
cmd/commandline/init.go

@@ -0,0 +1,157 @@
+package main
+
+import (
+	"os"
+	"path/filepath"
+	"time"
+
+	tea "github.com/charmbracelet/bubbletea"
+	"github.com/langgenius/dify-plugin-daemon/internal/types/entities/constants"
+	"github.com/langgenius/dify-plugin-daemon/internal/types/entities/plugin_entities"
+	"github.com/langgenius/dify-plugin-daemon/internal/utils/log"
+)
+
+type subMenuKey string
+
+const (
+	SUB_MENU_KEY_PROFILE    subMenuKey = "profile"
+	SUB_MENU_KEY_LANGUAGE   subMenuKey = "language"
+	SUB_MENU_KEY_PERMISSION subMenuKey = "permission"
+)
+
+type model struct {
+	subMenus       map[subMenuKey]subMenu
+	subMenuSeq     []subMenuKey
+	currentSubMenu subMenuKey
+}
+
+func initialize() model {
+	m := model{}
+	m.subMenus = map[subMenuKey]subMenu{
+		SUB_MENU_KEY_PROFILE:    newProfile(),
+		SUB_MENU_KEY_LANGUAGE:   newLanguage(),
+		SUB_MENU_KEY_PERMISSION: newPermission(),
+	}
+	m.currentSubMenu = SUB_MENU_KEY_PROFILE
+
+	m.subMenuSeq = []subMenuKey{
+		SUB_MENU_KEY_PROFILE,
+		SUB_MENU_KEY_LANGUAGE,
+		SUB_MENU_KEY_PERMISSION,
+	}
+
+	return m
+}
+
+func (m model) Init() tea.Cmd {
+	return nil
+}
+
+func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+	currentSubMenu, event, cmd := m.subMenus[m.currentSubMenu].Update(msg)
+	m.subMenus[m.currentSubMenu] = currentSubMenu
+
+	switch event {
+	case SUB_MENU_EVENT_NEXT:
+		if m.currentSubMenu != m.subMenuSeq[len(m.subMenuSeq)-1] {
+			// move the current sub menu to the next one
+			for i, key := range m.subMenuSeq {
+				if key == m.currentSubMenu {
+					m.currentSubMenu = m.subMenuSeq[i+1]
+					break
+				}
+			}
+		} else {
+			return m, tea.Quit
+		}
+	case SUB_MENU_EVENT_PREV:
+		if m.currentSubMenu != m.subMenuSeq[0] {
+			// move the current sub menu to the previous one
+			for i, key := range m.subMenuSeq {
+				if key == m.currentSubMenu {
+					m.currentSubMenu = m.subMenuSeq[i-1]
+					break
+				}
+			}
+		}
+	}
+
+	return m, cmd
+}
+
+func (m model) View() string {
+	return m.subMenus[m.currentSubMenu].View()
+}
+
+func (m model) createPlugin() {
+	permission := m.subMenus[SUB_MENU_KEY_PERMISSION].(permission).Permission()
+
+	manifest := &plugin_entities.PluginDeclaration{
+		Version:   "0.0.1",
+		Type:      plugin_entities.PluginType,
+		Author:    m.subMenus[SUB_MENU_KEY_PROFILE].(profile).Author(),
+		Name:      m.subMenus[SUB_MENU_KEY_PROFILE].(profile).Name(),
+		CreatedAt: time.Now(),
+		Resource: plugin_entities.PluginResourceRequirement{
+			Permission: &permission,
+		},
+	}
+
+	manifest.Meta = plugin_entities.PluginMeta{
+		Version: "0.0.1",
+		Arch: []constants.Arch{
+			constants.AMD64,
+			constants.ARM64,
+		},
+		Runner: plugin_entities.PluginRunner{},
+	}
+
+	switch m.subMenus[SUB_MENU_KEY_LANGUAGE].(language).Language() {
+	case constants.Python:
+		manifest.Meta.Runner.Entrypoint = "main"
+		manifest.Meta.Runner.Language = constants.Python
+		manifest.Meta.Runner.Version = "3.10"
+	default:
+		log.Error("unsupported language: %s", m.subMenus[SUB_MENU_KEY_LANGUAGE].(language).Language())
+		return
+	}
+
+	success := false
+
+	clear := func() {
+		if !success {
+			os.RemoveAll(manifest.Name)
+		}
+	}
+	defer clear()
+
+	manifest_file := marshalYamlBytes(manifest)
+	// create the plugin directory
+	cwd, err := os.Getwd()
+	if err != nil {
+		log.Error("failed to get current working directory: %s", err)
+		return
+	}
+
+	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 {
+		log.Error("failed to write manifest file: %s", err)
+		return
+	}
+
+	err = createPythonEnvironment(plugin_dir, manifest.Meta.Runner.Entrypoint)
+	if err != nil {
+		log.Error("failed to create python environment: %s", err)
+		return
+	}
+
+	success = true
+
+	log.Info("plugin %s created successfully", manifest.Name)
+}

+ 2 - 1
cmd/commandline/language.go

@@ -31,7 +31,7 @@ func (l language) View() string {
 	s := "Select the language you want to use for plugin development\n"
 	for i, language := range languages {
 		if i == l.cursor {
-			s += fmt.Sprintf("-> %s\n", language)
+			s += fmt.Sprintf("\033[32m-> %s\033[0m\n", language)
 		} else {
 			s += fmt.Sprintf("  %s\n", language)
 		}
@@ -57,6 +57,7 @@ func (l language) Update(msg tea.Msg) (subMenu, subMenuEvent, tea.Cmd) {
 			}
 		case "enter":
 			if l.cursor != 0 {
+				l.cursor = 0
 				return l, SUB_MENU_EVENT_NONE, nil
 			}
 

+ 11 - 72
cmd/commandline/main.go

@@ -4,81 +4,20 @@ import (
 	"fmt"
 
 	tea "github.com/charmbracelet/bubbletea"
+	"github.com/langgenius/dify-plugin-daemon/internal/utils/log"
 )
 
-type subMenuKey string
-
-const (
-	SUB_MENU_KEY_PROFILE    subMenuKey = "profile"
-	SUB_MENU_KEY_LANGUAGE   subMenuKey = "language"
-	SUB_MENU_KEY_PERMISSION subMenuKey = "permission"
-)
-
-type model struct {
-	subMenus       map[subMenuKey]subMenu
-	subMenuSeq     []subMenuKey
-	currentSubMenu subMenuKey
-}
-
-func initialize() model {
-	m := model{}
-	m.subMenus = map[subMenuKey]subMenu{
-		SUB_MENU_KEY_PROFILE:    newProfile(),
-		SUB_MENU_KEY_LANGUAGE:   newLanguage(),
-		SUB_MENU_KEY_PERMISSION: newPermission(),
-	}
-	m.currentSubMenu = SUB_MENU_KEY_PROFILE
-
-	m.subMenuSeq = []subMenuKey{
-		SUB_MENU_KEY_PROFILE,
-		SUB_MENU_KEY_LANGUAGE,
-		SUB_MENU_KEY_PERMISSION,
-	}
-
-	return m
-}
-
-func (m model) Init() tea.Cmd {
-	return nil
-}
-
-func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
-	currentSubMenu, event, cmd := m.subMenus[m.currentSubMenu].Update(msg)
-	m.subMenus[m.currentSubMenu] = currentSubMenu
-
-	switch event {
-	case SUB_MENU_EVENT_NEXT:
-		if m.currentSubMenu != m.subMenuSeq[len(m.subMenuSeq)-1] {
-			// move the current sub menu to the next one
-			for i, key := range m.subMenuSeq {
-				if key == m.currentSubMenu {
-					m.currentSubMenu = m.subMenuSeq[i+1]
-					break
-				}
-			}
-		}
-	case SUB_MENU_EVENT_PREV:
-		if m.currentSubMenu != m.subMenuSeq[0] {
-			// move the current sub menu to the previous one
-			for i, key := range m.subMenuSeq {
-				if key == m.currentSubMenu {
-					m.currentSubMenu = m.subMenuSeq[i-1]
-					break
-				}
-			}
-		}
-	}
-
-	return m, cmd
-}
-
-func (m model) View() string {
-	return m.subMenus[m.currentSubMenu].View()
-}
-
 func main() {
-	p := tea.NewProgram(initialize())
-	if _, err := p.Run(); err != nil {
+	m := initialize()
+	p := tea.NewProgram(m)
+	if result, err := p.Run(); err != nil {
 		fmt.Println("Error running program:", err)
+	} else {
+		if m, ok := result.(model); ok {
+			m.createPlugin()
+		} else {
+			log.Error("Error running program:", err)
+			return
+		}
 	}
 }

+ 4 - 0
cmd/commandline/permission.go

@@ -40,6 +40,10 @@ func newPermission() permission {
 	}
 }
 
+func (p permission) Permission() plugin_entities.PluginPermissionRequirement {
+	return p.permission
+}
+
 func (p permission) View() string {
 	cursor := func(key string) string {
 		if p.cursor == key {

+ 31 - 3
cmd/commandline/profile.go

@@ -10,6 +10,8 @@ import (
 type profile struct {
 	cursor int
 	inputs []ti.Model
+
+	warning string
 }
 
 func newProfile() profile {
@@ -38,7 +40,21 @@ func (p profile) Author() string {
 }
 
 func (p profile) View() string {
-	return fmt.Sprintf("Edit profile of the plugin\n%s\n%s\n", p.inputs[0].View(), p.inputs[1].View())
+	s := fmt.Sprintf("Edit profile of the plugin\n%s\n%s\n", p.inputs[0].View(), p.inputs[1].View())
+	if p.warning != "" {
+		s += fmt.Sprintf("\033[31m%s\033[0m\n", p.warning)
+	}
+	return s
+}
+
+func (p *profile) checkEmpty() bool {
+	if p.inputs[p.cursor].Value() == "" {
+		p.warning = "Name and author cannot be empty"
+		return false
+	} else {
+		p.warning = ""
+	}
+	return true
 }
 
 func (p profile) Update(msg tea.Msg) (subMenu, subMenuEvent, tea.Cmd) {
@@ -47,21 +63,33 @@ func (p profile) Update(msg tea.Msg) (subMenu, subMenuEvent, tea.Cmd) {
 	switch msg := msg.(type) {
 	case tea.KeyMsg:
 		switch msg.String() {
-		case "ctrl+c", "q":
+		case "ctrl+c":
 			return p, SUB_MENU_EVENT_NONE, tea.Quit
 		case "down":
+			// check if empty
+			if !p.checkEmpty() {
+				return p, SUB_MENU_EVENT_NONE, nil
+			}
+
 			// focus next
 			p.cursor++
 			if p.cursor >= len(p.inputs) {
 				p.cursor = 0
 			}
 		case "up":
-			// focus previous
+			if !p.checkEmpty() {
+				return p, SUB_MENU_EVENT_NONE, nil
+			}
+
 			p.cursor--
 			if p.cursor < 0 {
 				p.cursor = len(p.inputs) - 1
 			}
 		case "enter":
+			if !p.checkEmpty() {
+				return p, SUB_MENU_EVENT_NONE, nil
+			}
+
 			// submit
 			if p.cursor == len(p.inputs)-1 {
 				return p, SUB_MENU_EVENT_NEXT, nil

+ 29 - 0
cmd/commandline/python.go

@@ -0,0 +1,29 @@
+package main
+
+import (
+	_ "embed"
+	"fmt"
+	"os"
+	"path/filepath"
+)
+
+//go:embed templates/python/main.py
+var PYTHON_ENTRYPOINT_TEMPLATE []byte
+
+//go:embed templates/python/requirements.txt
+var PYTHON_REQUIREMENTS_TEMPLATE []byte
+
+func createPythonEnvironment(root string, entrypoint string) error {
+	// create the python environment
+	entrypoint_file_path := filepath.Join(root, fmt.Sprintf("%s.py", entrypoint))
+	if err := os.WriteFile(entrypoint_file_path, PYTHON_ENTRYPOINT_TEMPLATE, 0o644); err != nil {
+		return err
+	}
+
+	requirements_file_path := filepath.Join(root, "requirements.txt")
+	if err := os.WriteFile(requirements_file_path, PYTHON_REQUIREMENTS_TEMPLATE, 0o644); err != nil {
+		return err
+	}
+
+	return nil
+}

+ 6 - 0
cmd/commandline/templates/python/main.py

@@ -0,0 +1,6 @@
+from dify_plugin import Plugin, DifyPluginEnv
+
+plugin = Plugin(DifyPluginEnv(MAX_REQUEST_TIMEOUT=120))
+
+if __name__ == '__main__':
+    plugin.run()

+ 1 - 0
cmd/commandline/templates/python/requirements.txt

@@ -0,0 +1 @@
+dify_plugin~=0.0.1

+ 19 - 19
internal/types/entities/plugin_entities/plugin_declaration.go

@@ -18,12 +18,12 @@ const (
 )
 
 type PluginPermissionRequirement struct {
-	Tool     *PluginPermissionToolRequirement     `json:"tool" yaml:"tool" validate:"omitempty"`
-	Model    *PluginPermissionModelRequirement    `json:"model" yaml:"model" validate:"omitempty"`
-	Node     *PluginPermissionNodeRequirement     `json:"node" yaml:"node" validate:"omitempty"`
-	Endpoint *PluginPermissionEndpointRequirement `json:"endpoint" yaml:"endpoint" validate:"omitempty"`
-	App      *PluginPermissionAppRequirement      `json:"app" yaml:"app" validate:"omitempty"`
-	Storage  *PluginPermissionStorageRequirement  `json:"storage" yaml:"storage" validate:"omitempty"`
+	Tool     *PluginPermissionToolRequirement     `json:"tool,omitempty" yaml:"tool,omitempty" validate:"omitempty"`
+	Model    *PluginPermissionModelRequirement    `json:"model,omitempty" yaml:"model,omitempty" validate:"omitempty"`
+	Node     *PluginPermissionNodeRequirement     `json:"node,omitempty" yaml:"node,omitempty" validate:"omitempty"`
+	Endpoint *PluginPermissionEndpointRequirement `json:"endpoint,omitempty" yaml:"endpoint,omitempty" validate:"omitempty"`
+	App      *PluginPermissionAppRequirement      `json:"app,omitempty" yaml:"app,omitempty" validate:"omitempty"`
+	Storage  *PluginPermissionStorageRequirement  `json:"storage,omitempty" yaml:"storage,omitempty" validate:"omitempty"`
 }
 
 func (p *PluginPermissionRequirement) AllowInvokeTool() bool {
@@ -109,7 +109,7 @@ type PluginResourceRequirement struct {
 	// Memory in bytes
 	Memory int64 `json:"memory" yaml:"memory" validate:"required"`
 	// Permission requirements
-	Permission *PluginPermissionRequirement `json:"permission" yaml:"permission" validate:"omitempty"`
+	Permission *PluginPermissionRequirement `json:"permission,omitempty" yaml:"permission,omitempty" validate:"omitempty"`
 }
 
 type PluginDeclarationPlatformArch string
@@ -132,18 +132,18 @@ type PluginExecution struct {
 }
 
 type PluginDeclaration struct {
-	Version   string                       `json:"version" yaml:"version" validate:"required,version"`
-	Type      DifyManifestType             `json:"type" yaml:"type" validate:"required,eq=plugin"`
-	Author    string                       `json:"author" yaml:"author" validate:"required,max=128"`
-	Name      string                       `json:"name" yaml:"name" validate:"required,max=128" enum:"plugin"`
-	CreatedAt time.Time                    `json:"created_at" yaml:"created_at" validate:"required"`
-	Resource  PluginResourceRequirement    `json:"resource" yaml:"resource" validate:"required"`
-	Plugins   []string                     `json:"plugins" yaml:"plugins" validate:"required,dive,max=128"`
-	Execution PluginExecution              `json:"execution" yaml:"execution" validate:"required"`
-	Meta      PluginMeta                   `json:"meta" yaml:"meta" validate:"required"`
-	Endpoint  *EndpointProviderDeclaration `json:"endpoints" validate:"omitempty"`
-	Model     *ModelProviderConfiguration  `json:"models" validate:"omitempty"`
-	Tool      *ToolProviderConfiguration   `json:"tools" validate:"omitempty"`
+	Version   string                       `json:"version" yaml:"version,omitempty" validate:"required,version"`
+	Type      DifyManifestType             `json:"type" yaml:"type,omitempty" validate:"required,eq=plugin"`
+	Author    string                       `json:"author" yaml:"author,omitempty" validate:"required,max=128"`
+	Name      string                       `json:"name" yaml:"name,omitempty" validate:"required,max=128"`
+	CreatedAt time.Time                    `json:"created_at" yaml:"created_at,omitempty" validate:"required"`
+	Resource  PluginResourceRequirement    `json:"resource" yaml:"resource,omitempty" validate:"required"`
+	Plugins   []string                     `json:"plugins" yaml:"plugins,omitempty" validate:"required,dive,max=128"`
+	Execution PluginExecution              `json:"execution" yaml:"execution,omitempty" validate:"required"`
+	Meta      PluginMeta                   `json:"meta" yaml:"meta,omitempty" validate:"required"`
+	Endpoint  *EndpointProviderDeclaration `json:"-" yaml:"-" validate:"omitempty"`
+	Model     *ModelProviderConfiguration  `json:"-" yaml:"-" validate:"omitempty"`
+	Tool      *ToolProviderConfiguration   `json:"-" yaml:"-" validate:"omitempty"`
 }
 
 var (