Skip to content

Commit ae092ca

Browse files
feat: add subcommand extract to slim (#716)
* feat: add subcommand extract to slim * add download logic when extract * Update pkg/slim/extract.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * build and upload slim binrary when release --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 0a1ebc1 commit ae092ca

13 files changed

Lines changed: 1068 additions & 8 deletions

File tree

.github/workflows/publish-cli.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ jobs:
3737
CGO_ENABLED=0 GOOS=windows GOARCH=${{ env.GOARCH }} go build -ldflags "-X 'main.VersionX=v${{ github.event.release.tag_name }}'" -o dify-plugin-windows-${{ env.GOARCH }}.exe ./cmd/commandline
3838
CGO_ENABLED=0 GOOS=darwin GOARCH=${{ env.GOARCH }} go build -ldflags "-X 'main.VersionX=v${{ github.event.release.tag_name }}'" -o dify-plugin-darwin-${{ env.GOARCH }} ./cmd/commandline
3939
CGO_ENABLED=0 GOOS=linux GOARCH=${{ env.GOARCH }} go build -ldflags "-X 'main.VersionX=v${{ github.event.release.tag_name }}'" -o dify-plugin-linux-${{ env.GOARCH }} ./cmd/commandline
40+
CGO_ENABLED=0 GOOS=linux GOARCH=${{ env.GOARCH }} go build -o dify-plugin-slim-linux-${{ env.GOARCH }} ./cmd/slim
4041
4142
- name: Upload Artifacts
4243
uses: actions/upload-artifact@v4
@@ -50,6 +51,7 @@ jobs:
5051
run: |
5152
gh release upload ${{ github.event.release.tag_name }} dify-plugin-windows-${{ env.GOARCH }}.exe --clobber
5253
gh release upload ${{ github.event.release.tag_name }} dify-plugin-linux-${{ env.GOARCH }} --clobber
54+
gh release upload ${{ github.event.release.tag_name }} dify-plugin-slim-linux-${{ env.GOARCH }} --clobber
5355
env:
5456
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5557

cmd/slim/main.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"encoding/json"
66
"flag"
7+
"fmt"
78
"io"
89
"log/slog"
910
"os"
@@ -13,10 +14,16 @@ import (
1314

1415
func main() {
1516
slog.SetDefault(slog.New(slog.NewTextHandler(io.Discard, nil)))
17+
if len(os.Args) > 1 && os.Args[1] == "extract" {
18+
runExtractCommand(os.Args[2:])
19+
return
20+
}
21+
1622
id := flag.String("id", "", "plugin unique identifier")
1723
action := flag.String("action", "", "plugin access action")
1824
args := flag.String("args", "", "plugin invocation parameters (JSON); if omitted, read from stdin")
1925
configFile := flag.String("config", "", "path to JSON config file (replaces env vars)")
26+
flag.Usage = rootUsage
2027
flag.Parse()
2128

2229
if *id == "" || *action == "" {
@@ -66,6 +73,70 @@ func main() {
6673
}
6774
}
6875

76+
func runExtractCommand(args []string) {
77+
fs := flag.NewFlagSet("extract", flag.ContinueOnError)
78+
fs.SetOutput(os.Stderr)
79+
id := fs.String("id", "", "plugin unique identifier")
80+
action := fs.String("action", "", "generate -args data for this Slim action")
81+
path := fs.String("path", "", "local plugin directory or .difypkg path")
82+
output := fs.String("output", slim.OutputJSON, "output format")
83+
configFile := fs.String("config", "", "path to JSON config file (replaces env vars)")
84+
fs.Usage = func() {
85+
extractUsage(fs)
86+
}
87+
if err := fs.Parse(args); err != nil {
88+
if err == flag.ErrHelp {
89+
os.Exit(slim.ExitOK)
90+
}
91+
fatal(slim.NewError(slim.ErrInvalidInput, err.Error()))
92+
}
93+
if fs.NArg() != 0 {
94+
fatal(slim.NewError(slim.ErrInvalidInput, "unexpected positional arguments"))
95+
}
96+
97+
cfg, err := slim.LoadExtractConfig(*configFile, *path != "")
98+
if err != nil {
99+
fatal(err)
100+
}
101+
102+
if err := slim.RunExtract(cfg, slim.ExtractOptions{
103+
PluginID: *id,
104+
Action: *action,
105+
Path: *path,
106+
Output: *output,
107+
}, os.Stdout); err != nil {
108+
fatal(err)
109+
}
110+
}
111+
112+
func rootUsage() {
113+
w := flag.CommandLine.Output()
114+
fmt.Fprintln(w, "Usage:")
115+
fmt.Fprintln(w, " slim -id <plugin_unique_identifier> -action <action> [-args '<json>'] [-config <path>]")
116+
fmt.Fprintln(w, " slim extract -id <plugin_unique_identifier> [-config <path>] [-output json]")
117+
fmt.Fprintln(w, " slim extract -id <plugin_unique_identifier> -action <action> [-config <path>] [-output json]")
118+
fmt.Fprintln(w, " slim extract -path <plugin-dir-or-difypkg> [-config <path>] [-output json]")
119+
fmt.Fprintln(w, " slim extract -path <plugin-dir-or-difypkg> -action <action> [-config <path>] [-output json]")
120+
fmt.Fprintln(w)
121+
fmt.Fprintln(w, "Commands:")
122+
fmt.Fprintln(w, " extract Parse plugin schema/declaration from daemon or local files")
123+
fmt.Fprintln(w)
124+
fmt.Fprintln(w, "Invocation flags:")
125+
flag.PrintDefaults()
126+
}
127+
128+
func extractUsage(fs *flag.FlagSet) {
129+
w := fs.Output()
130+
fmt.Fprintln(w, "Usage:")
131+
fmt.Fprintln(w, " slim extract -id <plugin_unique_identifier> [-config <path>] [-output json]")
132+
fmt.Fprintln(w, " slim extract -id <plugin_unique_identifier> -action <action> [-config <path>] [-output json]")
133+
fmt.Fprintln(w, " slim extract -path <plugin-dir-or-difypkg> [-config <path>] [-output json]")
134+
fmt.Fprintln(w, " slim extract -path <plugin-dir-or-difypkg> -action <action> [-config <path>] [-output json]")
135+
fmt.Fprintln(w)
136+
fmt.Fprintln(w, "Extract flags:")
137+
fs.PrintDefaults()
138+
}
139+
69140
func fatal(err error) {
70141
exitCode := slim.ExitPluginError
71142
var errorToMarshal *slim.SlimError

internal/server/controllers/plugins.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/gin-gonic/gin"
99
"github.com/langgenius/dify-plugin-daemon/internal/core/plugin_manager"
10+
"github.com/langgenius/dify-plugin-daemon/internal/server/constants"
1011
"github.com/langgenius/dify-plugin-daemon/internal/service"
1112
"github.com/langgenius/dify-plugin-daemon/internal/types/app"
1213
"github.com/langgenius/dify-plugin-daemon/internal/types/exception"
@@ -235,6 +236,26 @@ func FetchPluginManifest(c *gin.Context) {
235236
})
236237
}
237238

239+
func ExtractPluginSchema(config *app.Config) gin.HandlerFunc {
240+
return func(c *gin.Context) {
241+
identityAny, ok := c.Get(constants.CONTEXT_KEY_PLUGIN_UNIQUE_IDENTIFIER)
242+
if !ok {
243+
c.JSON(http.StatusInternalServerError,
244+
exception.InternalServerError(errors.New("plugin unique identifier not found")).ToResponse())
245+
return
246+
}
247+
248+
identity, ok := identityAny.(plugin_entities.PluginUniqueIdentifier)
249+
if !ok {
250+
c.JSON(http.StatusInternalServerError,
251+
exception.InternalServerError(errors.New("failed to parse plugin unique identifier")).ToResponse())
252+
return
253+
}
254+
255+
c.JSON(http.StatusOK, service.ExtractPluginSchema(config, identity))
256+
}
257+
}
258+
238259
func FetchPluginReadme(c *gin.Context) {
239260
BindRequest(c, func(request struct {
240261
PluginUniqueIdentifier plugin_entities.PluginUniqueIdentifier `form:"plugin_unique_identifier" validate:"required,plugin_unique_identifier"`

internal/server/http_server.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,12 @@ func (app *App) pprofGroup(group *gin.RouterGroup, config *app.Config) {
226226

227227
func (app *App) invokeGroup(group *gin.RouterGroup, config *app.Config) {
228228
group.Use(CheckingKey(config.ServerKey))
229+
230+
extractGroup := group.Group("/extract")
231+
extractGroup.Use(app.FetchPluginDirect())
232+
extractGroup.Use(app.InitClusterID())
233+
extractGroup.GET("", controllers.ExtractPluginSchema(config))
234+
229235
dispatchGroup := group.Group("/dispatch")
230236
dispatchGroup.Use(controllers.CollectActiveDispatchRequests())
231237
dispatchGroup.Use(app.FetchPluginDirect())

internal/service/extract.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package service
2+
3+
import (
4+
"github.com/langgenius/dify-plugin-daemon/internal/core/plugin_manager"
5+
"github.com/langgenius/dify-plugin-daemon/internal/types/app"
6+
"github.com/langgenius/dify-plugin-daemon/internal/types/exception"
7+
"github.com/langgenius/dify-plugin-daemon/pkg/entities"
8+
"github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities"
9+
"github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/decoder"
10+
)
11+
12+
func ExtractPluginSchema(
13+
config *app.Config,
14+
pluginUniqueIdentifier plugin_entities.PluginUniqueIdentifier,
15+
) *entities.Response {
16+
manager := plugin_manager.Manager()
17+
pkgFile, err := manager.GetPackage(pluginUniqueIdentifier)
18+
if err != nil {
19+
return exception.BadRequestError(err).ToResponse()
20+
}
21+
22+
zipDecoder, err := decoder.NewZipPluginDecoderWithThirdPartySignatureVerificationConfig(
23+
pkgFile,
24+
&decoder.ThirdPartySignatureVerificationConfig{
25+
Enabled: config.ThirdPartySignatureVerificationEnabled,
26+
PublicKeyPaths: config.ThirdPartySignatureVerificationPublicKeys,
27+
},
28+
)
29+
if err != nil {
30+
return exception.BadRequestError(err).ToResponse()
31+
}
32+
defer zipDecoder.Close()
33+
34+
verification, _ := zipDecoder.Verification()
35+
if verification == nil && zipDecoder.Verified() {
36+
verification = decoder.DefaultVerification()
37+
}
38+
39+
declaration, err := zipDecoder.Manifest()
40+
if err != nil {
41+
return exception.BadRequestError(err).ToResponse()
42+
}
43+
44+
return entities.NewSuccessResponse(map[string]any{
45+
"unique_identifier": pluginUniqueIdentifier,
46+
"manifest": declaration,
47+
"verification": verification,
48+
})
49+
}

pkg/slim/config.go

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@ func NewInvokeContext(id, action, argsJSON string) (*InvokeContext, error) {
5959
}
6060

6161
func LoadConfigFromFile(path string) (*SlimConfig, error) {
62+
cfg, err := loadConfigFromFile(path)
63+
if err != nil {
64+
return nil, err
65+
}
66+
67+
if err := fillDefaults(cfg); err != nil {
68+
return nil, err
69+
}
70+
return cfg, nil
71+
}
72+
73+
func loadConfigFromFile(path string) (*SlimConfig, error) {
6274
data, err := os.ReadFile(path)
6375
if err != nil {
6476
return nil, NewError(ErrConfigLoad, err.Error())
@@ -69,13 +81,18 @@ func LoadConfigFromFile(path string) (*SlimConfig, error) {
6981
return nil, NewError(ErrConfigLoad, err.Error())
7082
}
7183

72-
if err := fillDefaults(&cfg); err != nil {
73-
return nil, err
74-
}
7584
return &cfg, nil
7685
}
7786

7887
func LoadConfig() (*SlimConfig, error) {
88+
cfg := loadConfigFromEnv()
89+
if err := fillDefaults(cfg); err != nil {
90+
return nil, err
91+
}
92+
return cfg, nil
93+
}
94+
95+
func loadConfigFromEnv() *SlimConfig {
7996
cfg := &SlimConfig{
8097
Mode: env("SLIM_MODE", ModeRemote),
8198
}
@@ -88,10 +105,10 @@ func LoadConfig() (*SlimConfig, error) {
88105
UvPath: env("SLIM_UV_PATH", ""),
89106
PythonEnvInitTimeout: envInt("SLIM_PYTHON_ENV_INIT_TIMEOUT", 0),
90107
MaxExecutionTimeout: envInt("SLIM_MAX_EXECUTION_TIMEOUT", 0),
91-
PipMirrorURL: env("SLIM_PIP_MIRROR_URL", ""),
92-
PipExtraArgs: env("SLIM_PIP_EXTRA_ARGS", ""),
93-
MarketplaceURL: env("SLIM_MARKETPLACE_URL", ""),
94-
IgnoreUvLock: envBool("SLIM_IGNORE_UV_LOCK", false),
108+
PipMirrorURL: env("SLIM_PIP_MIRROR_URL", ""),
109+
PipExtraArgs: env("SLIM_PIP_EXTRA_ARGS", ""),
110+
MarketplaceURL: env("SLIM_MARKETPLACE_URL", ""),
111+
IgnoreUvLock: envBool("SLIM_IGNORE_UV_LOCK", false),
95112
}
96113
case ModeRemote:
97114
cfg.Remote = RemoteConfig{
@@ -100,7 +117,22 @@ func LoadConfig() (*SlimConfig, error) {
100117
}
101118
}
102119

103-
if err := fillDefaults(cfg); err != nil {
120+
return cfg
121+
}
122+
123+
func LoadExtractConfig(configFile string, hasLocalPath bool) (*SlimConfig, error) {
124+
var cfg *SlimConfig
125+
var err error
126+
if configFile != "" {
127+
cfg, err = loadConfigFromFile(configFile)
128+
} else {
129+
cfg = loadConfigFromEnv()
130+
}
131+
if err != nil {
132+
return nil, err
133+
}
134+
135+
if err := fillExtractDefaults(cfg, hasLocalPath); err != nil {
104136
return nil, err
105137
}
106138
return cfg, nil
@@ -160,3 +192,30 @@ func fillDefaults(cfg *SlimConfig) error {
160192

161193
return nil
162194
}
195+
196+
func fillExtractDefaults(cfg *SlimConfig, hasLocalPath bool) error {
197+
if cfg.Mode == "" {
198+
cfg.Mode = ModeRemote
199+
}
200+
201+
switch cfg.Mode {
202+
case ModeLocal:
203+
if cfg.Local.Folder == "" && !hasLocalPath {
204+
return NewError(ErrConfigInvalid, "local.folder is required when extract uses -id")
205+
}
206+
if cfg.Local.MarketplaceURL == "" {
207+
cfg.Local.MarketplaceURL = "https://marketplace.dify.ai"
208+
}
209+
case ModeRemote:
210+
if cfg.Remote.DaemonAddr == "" {
211+
return NewError(ErrConfigInvalid, "remote.daemon_addr is required")
212+
}
213+
if cfg.Remote.DaemonKey == "" {
214+
return NewError(ErrConfigInvalid, "remote.daemon_key is required")
215+
}
216+
default:
217+
return NewError(ErrUnknownMode, cfg.Mode)
218+
}
219+
220+
return nil
221+
}

pkg/slim/config_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,41 @@ func TestLoadConfigFromFile_ValidationFails(t *testing.T) {
215215
t.Fatal("LoadConfigFromFile() should fail when remote config is incomplete")
216216
}
217217
}
218+
219+
func TestLoadExtractConfig_LocalPathDoesNotRequireFolder(t *testing.T) {
220+
dir := t.TempDir()
221+
cfgPath := filepath.Join(dir, "config.json")
222+
content := `{"mode": "local"}`
223+
if err := os.WriteFile(cfgPath, []byte(content), 0644); err != nil {
224+
t.Fatalf("WriteFile() error: %v", err)
225+
}
226+
227+
cfg, err := LoadExtractConfig(cfgPath, true)
228+
if err != nil {
229+
t.Fatalf("LoadExtractConfig() error: %v", err)
230+
}
231+
if cfg.Mode != ModeLocal {
232+
t.Fatalf("Mode = %q; want %q", cfg.Mode, ModeLocal)
233+
}
234+
}
235+
236+
func TestLoadExtractConfig_LocalIDRequiresFolder(t *testing.T) {
237+
dir := t.TempDir()
238+
cfgPath := filepath.Join(dir, "config.json")
239+
content := `{"mode": "local"}`
240+
if err := os.WriteFile(cfgPath, []byte(content), 0644); err != nil {
241+
t.Fatalf("WriteFile() error: %v", err)
242+
}
243+
244+
_, err := LoadExtractConfig(cfgPath, false)
245+
if err == nil {
246+
t.Fatal("LoadExtractConfig() should fail when local extract uses -id without folder")
247+
}
248+
se, ok := err.(*SlimError)
249+
if !ok {
250+
t.Fatalf("expected *SlimError, got %T", err)
251+
}
252+
if se.Code != ErrConfigInvalid {
253+
t.Fatalf("Code = %q; want %q", se.Code, ErrConfigInvalid)
254+
}
255+
}

0 commit comments

Comments
 (0)