mirror of
https://github.com/langgenius/dify-plugin-daemon.git
synced 2026-07-22 09:45:27 -04:00
7f463e32f6
* feat(plugin_decoder): add support for internationalized readme files - Introduced the AvailableI18nReadme method in the PluginDecoder interface to retrieve available readme files in multiple languages. - Implemented the method in FSPluginDecoder and ZipPluginDecoder to read localized readme files from the filesystem and zip archives. - Enhanced UnixPluginDecoder to handle readme files in a structured manner, including support for reading from a dedicated "readme" directory. - Added unit tests to verify the functionality of the AvailableI18nReadme method and ensure correct retrieval of localized readme content. * feat(plugin): add support for multilingual README generation - Introduced functionality to create README files in multiple languages (Simplified Chinese, Japanese, Portuguese) based on user selection. - Enhanced the profile management to include options for enabling internationalized README and selecting languages. - Added new language choice structure to manage language options and their selection state. - Implemented rendering and writing of language-specific README files during plugin creation. - Included new README template files for each supported language. * feat(plugin): add README command and list functionality - Introduced a new `readme` command to the plugin CLI for managing README files. - Added `list` subcommand to display available README languages for a specified plugin path. - Implemented functionality to read and list supported README languages in a tabular format. - Enhanced error handling for plugin file reading and decoding processes.
341 lines
8.4 KiB
Go
341 lines
8.4 KiB
Go
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
|
|
|
|
enableI18nReadme bool
|
|
languageChoices []languageChoice
|
|
languageSection bool // true when in language selection section
|
|
warning string
|
|
}
|
|
|
|
func newProfile() profile {
|
|
name := ti.New()
|
|
name.Placeholder = "Plugin name, a directory will be created with this name"
|
|
name.CharLimit = 128
|
|
name.Prompt = "Plugin name (press Enter to next step): "
|
|
name.Focus()
|
|
|
|
author := ti.New()
|
|
author.Placeholder = "Author name"
|
|
author.CharLimit = 128
|
|
author.Prompt = "Author (press Enter to next step): "
|
|
|
|
description := ti.New()
|
|
description.Placeholder = "Description"
|
|
description.CharLimit = 1024
|
|
description.Prompt = "Description (press Enter to next step): "
|
|
|
|
repo := ti.New()
|
|
repo.Placeholder = "Repository URL (Optional)"
|
|
repo.CharLimit = 128
|
|
repo.Prompt = "Repository URL (Optional) (press Enter to next step): "
|
|
|
|
return profile{
|
|
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,
|
|
}
|
|
}
|
|
|
|
func (p profile) Name() string {
|
|
return p.inputs[0].Value()
|
|
}
|
|
|
|
func (p profile) Author() string {
|
|
return p.inputs[1].Value()
|
|
}
|
|
|
|
func (p profile) Description() string {
|
|
return p.inputs[2].Value()
|
|
}
|
|
|
|
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 {
|
|
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.WriteString(fmt.Sprintf("\n\033[31m%s\033[0m\n", p.warning))
|
|
}
|
|
|
|
return s.String()
|
|
}
|
|
|
|
func (p *profile) checkRule() bool {
|
|
if p.cursor >= 0 && p.cursor <= 2 && p.inputs[p.cursor].Value() == "" {
|
|
p.warning = "Name, author and description cannot be empty"
|
|
return false
|
|
} else if p.cursor == 0 && !plugin_entities.PluginNameRegex.MatchString(p.inputs[p.cursor].Value()) {
|
|
p.warning = "Plugin name must be 1-128 characters long, and can only contain lowercase letters, numbers, dashes and underscores"
|
|
return false
|
|
} else if p.cursor == 1 && !plugin_entities.AuthorRegex.MatchString(p.inputs[p.cursor].Value()) {
|
|
p.warning = "Author name must be 1-64 characters long, and can only contain lowercase letters, numbers, dashes and underscores"
|
|
return false
|
|
} else {
|
|
p.warning = ""
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (p profile) Update(msg tea.Msg) (subMenu, subMenuEvent, tea.Cmd) {
|
|
var cmds []tea.Cmd
|
|
|
|
switch msg := msg.(type) {
|
|
case tea.KeyMsg:
|
|
switch msg.String() {
|
|
case "ctrl+c":
|
|
return p, SUB_MENU_EVENT_NONE, tea.Quit
|
|
case "down":
|
|
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
|
|
}
|
|
|
|
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.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 {
|
|
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.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 == 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 (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 (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)
|
|
}
|
|
}
|
|
}
|
|
|
|
return p, SUB_MENU_EVENT_NONE, tea.Batch(cmds...)
|
|
}
|
|
|
|
func (p profile) Init() tea.Cmd {
|
|
return nil
|
|
}
|
|
|
|
func (p *profile) SetAuthor(author string) {
|
|
p.inputs[1].SetValue(author)
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|