Files
dingtalk-workspace-cli/internal/app/plugin_cmd.go
T
修雨 c4fb1bbd3e feat(plugin): add plugin system core framework
Introduce the plugin system for DWS CLI, enabling official and third-party
plugin management. This includes plugin manifest parsing/validation, loader
with managed/user directory-based identity, MCP server conversion and
injection into the dynamic routing registry, pipeline hook adapter for
shell-based hooks, and CLI commands (list/install/info/enable/disable/
remove/validate).
2026-04-16 09:33:20 +08:00

229 lines
6.8 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package app
import (
"fmt"
"strings"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/plugin"
"github.com/spf13/cobra"
)
func newPluginCommand() *cobra.Command {
pluginCmd := newPlaceholderParent("plugin", "插件管理")
pluginCmd.AddCommand(
newPluginListCommand(),
newPluginInstallCommand(),
newPluginInfoCommand(),
newPluginEnableCommand(),
newPluginDisableCommand(),
newPluginRemoveCommand(),
newPluginValidateCommand(),
)
return pluginCmd
}
func newPluginListCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "列出已安装插件",
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
loader := plugin.NewLoader(RawVersion())
plugins := loader.ListInstalled()
wantJSON, _ := cmd.Flags().GetBool("json")
if wantJSON {
return output.WriteJSON(cmd.OutOrStdout(), plugins)
}
if len(plugins) == 0 {
fmt.Fprintln(cmd.OutOrStdout(), "暂无已安装插件")
return nil
}
w := cmd.OutOrStdout()
fmt.Fprintf(w, "%-30s %-10s %-10s %-8s %s\n",
"插件名", "版本", "类型", "状态", "描述")
fmt.Fprintln(w, strings.Repeat("─", 80))
for _, p := range plugins {
pType := "三方"
if p.Type == "managed" {
pType = "官方"
}
status := "启用"
if !p.Enabled {
status = "禁用"
}
fmt.Fprintf(w, "%-30s %-10s %-10s %-8s %s\n",
p.Name, p.Version, pType, status, p.Description)
}
return nil
},
}
cmd.Flags().Bool("json", false, "以 JSON 格式输出")
return cmd
}
func newPluginInstallCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "install",
Short: "安装插件",
Example: ` dws plugin install --dir ./conference
dws plugin install --git https://github.com/DingTalk-Real-AI/conference.git`,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
dirPath, _ := cmd.Flags().GetString("dir")
gitURL, _ := cmd.Flags().GetString("git")
if dirPath == "" && gitURL == "" {
return apperrors.NewValidation("请指定安装来源:--dir <目录> 或 --git <仓库地址>")
}
if gitURL != "" {
return apperrors.NewValidation("git 安装暂未实现,请使用 --dir 从本地目录安装")
}
loader := plugin.NewLoader(RawVersion())
p, err := loader.InstallFromDir(dirPath)
if err != nil {
return apperrors.NewInternal(fmt.Sprintf("安装失败: %v", err))
}
fmt.Fprintf(cmd.OutOrStdout(), "✅ 插件 %s (%s) 安装成功\n", p.Manifest.Name, p.Manifest.Version)
return nil
},
}
cmd.Flags().String("dir", "", "从本地目录安装插件")
cmd.Flags().String("git", "", "从 Git 仓库安装插件")
return cmd
}
func newPluginInfoCommand() *cobra.Command {
return &cobra.Command{
Use: "info <插件名>",
Short: "查看插件详情",
Args: cobra.ExactArgs(1),
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
loader := plugin.NewLoader(RawVersion())
plugins := loader.ListInstalled()
for _, p := range plugins {
if p.Name == name {
w := cmd.OutOrStdout()
fmt.Fprintf(w, "插件名: %s\n", p.Name)
fmt.Fprintf(w, "版本: %s\n", p.Version)
fmt.Fprintf(w, "类型: %s\n", p.Type)
fmt.Fprintf(w, "状态: %s\n", enabledStr(p.Enabled))
fmt.Fprintf(w, "路径: %s\n", p.Path)
if p.Description != "" {
fmt.Fprintf(w, "描述: %s\n", p.Description)
}
return nil
}
}
return apperrors.NewValidation(fmt.Sprintf("插件 %q 未安装", name))
},
}
}
func newPluginEnableCommand() *cobra.Command {
return &cobra.Command{
Use: "enable <插件名>",
Short: "启用插件",
Args: cobra.ExactArgs(1),
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
loader := plugin.NewLoader(RawVersion())
if err := loader.SetEnabled(args[0], true); err != nil {
return apperrors.NewValidation(err.Error())
}
fmt.Fprintf(cmd.OutOrStdout(), "✅ 插件 %s 已启用\n", args[0])
return nil
},
}
}
func newPluginDisableCommand() *cobra.Command {
return &cobra.Command{
Use: "disable <插件名>",
Short: "禁用插件(官方插件可禁用但不可卸载)",
Args: cobra.ExactArgs(1),
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
loader := plugin.NewLoader(RawVersion())
if err := loader.SetEnabled(args[0], false); err != nil {
return apperrors.NewValidation(err.Error())
}
fmt.Fprintf(cmd.OutOrStdout(), "✅ 插件 %s 已禁用\n", args[0])
return nil
},
}
}
func newPluginRemoveCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "remove <插件名>",
Short: "卸载三方插件(官方插件禁止卸载)",
Args: cobra.ExactArgs(1),
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
keepData, _ := cmd.Flags().GetBool("keep-data")
loader := plugin.NewLoader(RawVersion())
if err := loader.RemovePlugin(args[0], keepData); err != nil {
return apperrors.NewValidation(err.Error())
}
fmt.Fprintf(cmd.OutOrStdout(), "✅ 插件 %s 已卸载\n", args[0])
return nil
},
}
cmd.Flags().Bool("keep-data", false, "保留插件数据目录")
return cmd
}
func newPluginValidateCommand() *cobra.Command {
return &cobra.Command{
Use: "validate <目录>",
Short: "校验 plugin.json",
Args: cobra.ExactArgs(1),
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
dir := args[0]
m, err := plugin.ParseManifest(dir + "/plugin.json")
if err != nil {
return apperrors.NewValidation(fmt.Sprintf("解析失败: %v", err))
}
if err := m.Validate(RawVersion()); err != nil {
return apperrors.NewValidation(fmt.Sprintf("校验失败: %v", err))
}
fmt.Fprintf(cmd.OutOrStdout(), "✅ plugin.json 校验通过: %s (%s)\n", m.Name, m.Version)
return nil
},
}
}
func enabledStr(enabled bool) string {
if enabled {
return "启用"
}
return "禁用"
}