@pippit-dev/cli 1.0.31 → 1.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cmd/model.go CHANGED
@@ -16,10 +16,7 @@ func newModelCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Com
16
16
  var modelType string
17
17
  service := models.NewService(runner)
18
18
  query := func(cmd *cobra.Command, args []string, describe bool) error {
19
- if modelType != "video" {
20
- return fmt.Errorf("当前仅支持 --type video")
21
- }
22
- result, err := service.Get(cmd.Context(), refresh)
19
+ result, err := service.Get(cmd.Context(), modelType, refresh)
23
20
  if err != nil {
24
21
  return err
25
22
  }
@@ -43,9 +40,9 @@ func newModelCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Com
43
40
  return common.WriteJSON(stdout, output)
44
41
  }
45
42
  cmd := &cobra.Command{
46
- Use: "model [key]",
47
- Short: "Discover available video models and their server configuration",
48
- Long: "Query available video models using your current credentials. Successful queries are cached locally for 5 minutes. Use --refresh to bypass the cache; retry if a query fails.",
43
+ Use: "model [name-or-key]",
44
+ Short: "Discover available image and video models and their server configuration",
45
+ Long: "Query available image and video models using your current credentials. Select images by their full display name (in quotes), videos by key. Successful queries are cached locally for 5 minutes. Use --refresh to bypass the cache; retry if a query fails.",
49
46
  Args: cobra.MaximumNArgs(1),
50
47
  RunE: withErrorLog("model", nil, func(cmd *cobra.Command, args []string) error {
51
48
  return query(cmd, args, len(args) > 0)
@@ -54,17 +51,17 @@ func newModelCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Com
54
51
  cmd.SetOut(stdout)
55
52
  cmd.SetErr(stderr)
56
53
  cmd.PersistentFlags().BoolVar(&refresh, "refresh", false, "refresh the 5-minute local model cache")
57
- cmd.PersistentFlags().StringVarP(&modelType, "type", "t", "video", "model type (currently video only)")
54
+ cmd.PersistentFlags().StringVarP(&modelType, "type", "t", "video", "model type: video or image (default video)")
58
55
  cmd.AddCommand(&cobra.Command{
59
56
  Use: "list [query]", Aliases: []string{"search"},
60
- Short: "List video models, optionally filtered by key or name",
57
+ Short: "List models; search images by name, videos by key or name",
61
58
  Args: cobra.MaximumNArgs(1),
62
59
  RunE: withErrorLog("model list", nil, func(cmd *cobra.Command, args []string) error {
63
60
  return query(cmd, args, false)
64
61
  }),
65
62
  })
66
63
  cmd.AddCommand(&cobra.Command{
67
- Use: "describe <key>", Short: "Show model parameters with CLI-ready ratios, defaults, and limits",
64
+ Use: "describe <name-or-key>", Short: "Show model parameters; use the full image name or video key",
68
65
  Args: cobra.ExactArgs(1),
69
66
  RunE: withErrorLog("model describe", nil, func(cmd *cobra.Command, args []string) error {
70
67
  return query(cmd, args, true)
package/cmd/model_test.go CHANGED
@@ -67,7 +67,7 @@ func TestModelCommands(t *testing.T) {
67
67
  }
68
68
 
69
69
  func TestModelHelpDoesNotRequireLoginOrRequest(t *testing.T) {
70
- for _, args := range [][]string{{"model", "--help"}, {"generate-video", "--help"}} {
70
+ for _, args := range [][]string{{"model", "--help"}, {"generate-video", "--help"}, {"generate-image", "--help"}} {
71
71
  var stdout, stderr bytes.Buffer
72
72
  root := NewRootCommand(&stdout, &stderr)
73
73
  root.SetArgs(args)
@@ -79,3 +79,61 @@ func TestModelHelpDoesNotRequireLoginOrRequest(t *testing.T) {
79
79
  }
80
80
  }
81
81
  }
82
+
83
+ func TestImageModelCommands(t *testing.T) {
84
+ t.Setenv("HOME", t.TempDir())
85
+ t.Setenv("XDG_CACHE_HOME", t.TempDir())
86
+ t.Setenv("LocalAppData", t.TempDir())
87
+ requests := 0
88
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
89
+ requests++
90
+ var body map[string]string
91
+ if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body["scene"] != "web_image_agent" || body["source_model_key"] != "" || r.URL.Path != config.GetAvailableModelListPath {
92
+ t.Errorf("unexpected request: body=%v path=%s err=%v", body, r.URL.Path, err)
93
+ }
94
+ _, _ = w.Write([]byte(`{"ret":"0","data":{"scene":"web_image_agent","config_key":"image-config","config":{"models":[{"key":"image-model","report_name":"image-model","name":"图片测试模型","kind":"image","is_default":true,"supported_ratio_list":[0,2,6],"parameter_config":{"dimensions":[{"key":"resolution","default_value":"2K","option_list":[{"value":"2K"}]},{"key":"effort","default_value":"low","option_list":[{"value":"low"},{"value":"high"}]}]}}]}}}`))
95
+ }))
96
+ defer server.Close()
97
+ for _, args := range [][]string{
98
+ {"model", "list", "--type", "image"},
99
+ {"model", "search", "图片", "-t", "image"},
100
+ {"model", "describe", "图片测试模型", "--type", "image"},
101
+ {"model", "图片测试模型", "-t", "image"},
102
+ } {
103
+ var stdout, stderr bytes.Buffer
104
+ cfg := config.Load()
105
+ cfg.BaseURL, cfg.AccessKey = server.URL, "image-model-test-key"
106
+ root := newRootCommand(&stdout, &stderr, newRootRunner(cfg))
107
+ root.SetArgs(args)
108
+ if err := root.Execute(); err != nil {
109
+ t.Fatalf("%v: %v", args, err)
110
+ }
111
+ if !strings.Contains(stdout.String(), `"scene": "web_image_agent"`) && !strings.Contains(stdout.String(), `"scene":"web_image_agent"`) {
112
+ t.Fatalf("missing image scene: %s", stdout.String())
113
+ }
114
+ if strings.Contains(stdout.String(), `"is_default"`) || strings.Contains(stdout.String(), `"config_key"`) || strings.Contains(stdout.String(), "image-model") {
115
+ t.Fatalf("internal fields exposed: %s", stdout.String())
116
+ }
117
+ if args[1] == "describe" || args[1] == "图片测试模型" {
118
+ var output struct {
119
+ Model struct {
120
+ Effort struct{ Options []string }
121
+ }
122
+ }
123
+ if err := json.Unmarshal(stdout.Bytes(), &output); err != nil || len(output.Model.Effort.Options) != 2 {
124
+ t.Fatalf("missing effort choices: %s err=%v", stdout.String(), err)
125
+ }
126
+ }
127
+ }
128
+ if requests != 1 {
129
+ t.Fatalf("commands did not share image cache: %d requests", requests)
130
+ }
131
+ var stdout, stderr bytes.Buffer
132
+ cfg := config.Load()
133
+ cfg.BaseURL, cfg.AccessKey = server.URL, "image-model-test-key"
134
+ root := newRootCommand(&stdout, &stderr, newRootRunner(cfg))
135
+ root.SetArgs([]string{"model", "list", "--type", "audio"})
136
+ if err := root.Execute(); err == nil || requests != 1 {
137
+ t.Fatalf("invalid type must fail before request: err=%v requests=%d", err, requests)
138
+ }
139
+ }
package/cmd/root.go CHANGED
@@ -73,6 +73,7 @@ func newRootCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Comm
73
73
  root.AddCommand(newDownloadResultCommand(stdout, stderr, runner))
74
74
  root.AddCommand(newGetCreditBalanceCommand(stdout, stderr, runner))
75
75
  root.AddCommand(newModelCommand(stdout, stderr, runner))
76
+ root.AddCommand(newMarketingCommand(stdout, stderr, runner))
76
77
  root.AddCommand(newGetThreadCommand(stdout, stderr, runner))
77
78
  root.AddCommand(newSubmitRunCommand(stdout, stderr, runner))
78
79
  root.AddCommand(newUploadFileCommand(stdout, stderr, runner))
@@ -11,6 +11,7 @@ import (
11
11
  "testing"
12
12
  "time"
13
13
 
14
+ "github.com/Pippit-dev/pippit-cli/internal/auth"
14
15
  "github.com/Pippit-dev/pippit-cli/internal/common"
15
16
  "github.com/Pippit-dev/pippit-cli/internal/config"
16
17
  "github.com/Pippit-dev/pippit-cli/internal/version"
@@ -963,6 +964,7 @@ func newTestRootCommandWithAccessKey(t *testing.T, stdout, stderr io.Writer, bas
963
964
  cfg.AccessKey = accessKey
964
965
  client := common.NewHTTPClient(cfg.BaseURL, cfg.HTTPTimeout, common.NewAccessKeyAuthorizer(cfg.AccessKey))
965
966
  runner := common.NewRunner(cfg, client)
967
+ runner.Auth = auth.NewManager(cfg)
966
968
  return newRootCommand(stdout, stderr, runner)
967
969
  }
968
970
 
@@ -16,6 +16,9 @@ import (
16
16
  )
17
17
 
18
18
  func TestSkillSubmitSourcePreservesCreativeRequest(t *testing.T) {
19
+ t.Setenv("HOME", t.TempDir())
20
+ t.Setenv("XDG_CACHE_HOME", t.TempDir())
21
+ t.Setenv("LocalAppData", t.TempDir())
19
22
  for _, name := range []string{"submit-run", "generate-image", "generate-video", "video-super-resolution", "erase-video-subtitle", "short-drama"} {
20
23
  t.Run(name, func(t *testing.T) {
21
24
  video := filepath.Join(t.TempDir(), "input.mp4")
@@ -24,7 +27,7 @@ func TestSkillSubmitSourcePreservesCreativeRequest(t *testing.T) {
24
27
  }
25
28
  args := map[string][]string{
26
29
  "submit-run": {"submit-run", "--message", " 保留原文\nworkbuddy 只是内容 ", "--thread-id", "skill_existing", "--asset-ids", "asset_1"},
27
- "generate-image": {"generate-image", "--prompt", "cat", "--model", "seedream_5.0_pro", "--ratio", "6", "--resolution", "4K"},
30
+ "generate-image": {"generate-image", "--prompt", "cat", "--model", "Seedream 5.0 Pro", "--ratio", "6", "--resolution", "4K"},
28
31
  "generate-video": {"generate-video", "--prompt", "cat", "--model", "Seedance_2.0_mini", "--duration", "4", "--ratio", "16:9"},
29
32
  "video-super-resolution": {"video-super-resolution", "--video", video, "--output-resolution", "1080p"},
30
33
  "erase-video-subtitle": {"erase-video-subtitle", "--video", video},
@@ -37,6 +40,8 @@ func TestSkillSubmitSourcePreservesCreativeRequest(t *testing.T) {
37
40
  uploadFailure := false
38
41
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
39
42
  switch r.URL.Path {
43
+ case config.GetAvailableModelListPath:
44
+ _, _ = w.Write([]byte(`{"ret":"0","data":{"scene":"web_image_agent","config_key":"image-key","config":{"models":[{"key":"seedream_5.0_pro","name":"Seedream 5.0 Pro","kind":"image"}]}}}`))
40
45
  case config.SubmitRunPath:
41
46
  submitCount++
42
47
  if r.Method != http.MethodPost || r.Header.Get("Authorization") != "Bearer test-token" {
@@ -1,6 +1,6 @@
1
- 0023facaf65dc042d7dd08b285efbd046efd3dd4cebd29f31e3421b919d38bc9 pippit-tool-cli-1.0.31-darwin-amd64.tar.gz
2
- a6989ce590556924da7a2ceb5d94a8d75f81b459dae5fe016d0d929ae9c31a0f pippit-tool-cli-1.0.31-darwin-arm64.tar.gz
3
- 2ccc7bfb01fc00a0a7c2948abb18eda6da0769c7c1f21bca3db942a1e7b3a0b3 pippit-tool-cli-1.0.31-linux-amd64.tar.gz
4
- 2a6c10aada4090701a97ef8af69ad91abf4d0fda000634c344157487c7895da7 pippit-tool-cli-1.0.31-linux-arm64.tar.gz
5
- fcf2e3e988952e046e23a4505630d758e7471d6d7125541eba7d5882588b67cb pippit-tool-cli-1.0.31-windows-amd64.zip
6
- 5b9ce586c808866404c3d6e006c9ed359d1d8aecee105ac38e0015b79be78d64 pippit-tool-cli-1.0.31-windows-arm64.zip
1
+ dd742dedf06e2645d02bc13a4cfa1af45778ae0200a8a6e67df177ba7e069775 pippit-tool-cli-1.0.33-darwin-amd64.tar.gz
2
+ 8d9fc0112650839c430e58cb7d6917e5bb1b95fde49659aecbbf14d1421c28f5 pippit-tool-cli-1.0.33-darwin-arm64.tar.gz
3
+ f2f2f741c920b115b2a1a9c4c4f074c1a07fd6cbc3a442cf4dc4d793c1b9ef5a pippit-tool-cli-1.0.33-linux-amd64.tar.gz
4
+ 2ed6e791c7dbea0e2ea8a1d05a0db298442218ecfc17ec47d70d49639f1d91b6 pippit-tool-cli-1.0.33-linux-arm64.tar.gz
5
+ f713c05325043f177f4a9e01a433b6a7fc8f57fea123f712ee459707955d199e pippit-tool-cli-1.0.33-windows-amd64.zip
6
+ 7e637f67fbbb818db0e66c49f7814d7e79cd71857d8c3bd6a5397af9d18533ce pippit-tool-cli-1.0.33-windows-arm64.zip
@@ -46,7 +46,17 @@ func NewHTTPClient(baseURL string, timeout time.Duration, authorizer RequestAuth
46
46
  return newHTTPClient(baseURL, timeout, authorizer)
47
47
  }
48
48
 
49
- func newHTTPClient(baseURL string, timeout time.Duration, authorizer RequestAuthorizer) Client {
49
+ // NewNonRedirectingHTTPClient keeps one-shot submissions from being replayed.
50
+ // Authentication and request handling still use the shared CLI client.
51
+ func NewNonRedirectingHTTPClient(baseURL string, timeout time.Duration, authorizer RequestAuthorizer) Client {
52
+ client := newHTTPClient(baseURL, timeout, authorizer)
53
+ client.httpClient.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
54
+ return fmt.Errorf("拒绝营销 API 重定向;请求未重试")
55
+ }
56
+ return client
57
+ }
58
+
59
+ func newHTTPClient(baseURL string, timeout time.Duration, authorizer RequestAuthorizer) *httpClient {
50
60
  client := &httpClient{
51
61
  baseURL: strings.TrimRight(baseURL, "/"),
52
62
  headers: make(http.Header),
@@ -0,0 +1,12 @@
1
+ package common
2
+
3
+ // RatioValue mirrors capcut_business_common.Ratio. Custom (1) has no CLI
4
+ // width/height parameters, so it and unknown enums have no display value.
5
+ func RatioValue(value int64) string {
6
+ return ratioValues[value]
7
+ }
8
+
9
+ var ratioValues = map[int64]string{
10
+ 0: "adaptive", 2: "16:9", 3: "9:16", 4: "4:3", 5: "3:4", 6: "1:1",
11
+ 7: "2:1", 8: "2.35:1", 9: "1.85:1", 10: "1.125:2.436", 11: "3:2", 12: "2:3", 13: "21:9",
12
+ }
@@ -8,6 +8,7 @@ import (
8
8
  "strings"
9
9
 
10
10
  "github.com/Pippit-dev/pippit-cli/internal/common"
11
+ "github.com/Pippit-dev/pippit-cli/internal/models"
11
12
  )
12
13
 
13
14
  const (
@@ -19,7 +20,7 @@ var (
19
20
  allowedImageExtensions = common.StringSet(allowedImageExtensionList)
20
21
  )
21
22
 
22
- const ratioUsage = "enum values: 0=原始比例/自动, 2=16:9(横屏), 13=21:9(电影), 3=9:16(竖屏), 4=4:3, 5=3:4, 6=1:1"
23
+ const ratioUsage = "integer enum from 'model describe \"MODEL_NAME\" --type image' (e.g. 0=adaptive, 2=16:9, 3=9:16, 6=1:1)"
23
24
 
24
25
  // Options is the stable command-facing request shape for generate-image.
25
26
  type Options struct {
@@ -29,6 +30,7 @@ type Options struct {
29
30
  Model string
30
31
  Ratio string
31
32
  Resolution string
33
+ Effort string
32
34
  GenerateImageCount *int
33
35
  }
34
36
 
@@ -36,6 +38,7 @@ type generalAgentSettings struct {
36
38
  ImageModel string `json:"image_model"`
37
39
  Ratio *int `json:"ratio,omitempty"`
38
40
  Resolution string `json:"resolution,omitempty"`
41
+ ImageEffort string `json:"image_effort,omitempty"`
39
42
  GenerateImageCount *int `json:"generate_image_count,omitempty"`
40
43
  }
41
44
 
@@ -53,23 +56,31 @@ func Run(ctx context.Context, opts *Options, runner *common.Runner) (*Result, er
53
56
  if err := ValidateOptions(opts); err != nil {
54
57
  return nil, err
55
58
  }
59
+ catalog, err := models.NewService(runner).Get(ctx, "image", false)
60
+ if err != nil {
61
+ return nil, err
62
+ }
63
+ modelKey, err := catalog.Catalog.ImageModelKey(opts.Model)
64
+ if err != nil {
65
+ return nil, err
66
+ }
56
67
 
57
68
  imageAssetIDs, err := uploadImageList(ctx, opts.ImagePaths, runner)
58
69
  if err != nil {
59
70
  return nil, fmt.Errorf("上传图片失败: %w", err)
60
71
  }
61
72
 
62
- body := buildSubmitRunBody(opts, imageAssetIDs)
73
+ body := buildSubmitRunBody(opts, modelKey, imageAssetIDs)
63
74
 
64
75
  var resp common.SubmitRunResponse
65
76
  if err := runner.Client.SendRequest(ctx, common.SubmitRunPath(runner), body, &resp); err != nil {
66
- return nil, fmt.Errorf("提交 generate-image 请求失败: %w", err)
77
+ return nil, fmt.Errorf("提交 generate-image 请求失败: %s", catalog.Catalog.ImageDisplayMessage(err.Error()))
67
78
  }
68
79
  if resp.Ret != "0" {
69
80
  if resp.Errmsg == "" {
70
81
  resp.Errmsg = "未知错误"
71
82
  }
72
- return nil, common.NewLogIDError(fmt.Sprintf("generate-image 请求返回失败: ret=%s errmsg=%s", resp.Ret, resp.Errmsg), resp.LogID)
83
+ return nil, common.NewLogIDError(fmt.Sprintf("generate-image 请求返回失败: ret=%s errmsg=%s", resp.Ret, catalog.Catalog.ImageDisplayMessage(resp.Errmsg)), resp.LogID)
73
84
  }
74
85
  if resp.Data.Run.ThreadID == "" {
75
86
  return nil, fmt.Errorf("generate-image 响应缺少 data.run.thread_id")
@@ -137,15 +148,16 @@ func uploadImageList(ctx context.Context, paths []string, runner *common.Runner)
137
148
  return assetIDs, nil
138
149
  }
139
150
 
140
- func buildSubmitRunBody(opts *Options, imageAssetIDs []string) map[string]any {
151
+ func buildSubmitRunBody(opts *Options, modelKey string, imageAssetIDs []string) map[string]any {
141
152
  ratio, _ := parseRatio(opts.Ratio)
142
153
  body := map[string]any{
143
154
  "agent_name": agentNameNest,
144
155
  "message": strings.TrimSpace(opts.Prompt),
145
156
  "general_agent_settings": generalAgentSettings{
146
- ImageModel: strings.TrimSpace(opts.Model),
157
+ ImageModel: modelKey,
147
158
  Ratio: ratio,
148
159
  Resolution: strings.ToUpper(strings.TrimSpace(opts.Resolution)),
160
+ ImageEffort: strings.ToLower(strings.TrimSpace(opts.Effort)),
149
161
  GenerateImageCount: opts.GenerateImageCount,
150
162
  },
151
163
  }
@@ -162,7 +174,7 @@ func parseRatio(raw string) (*int, error) {
162
174
  }
163
175
  value, err := strconv.Atoi(ratio)
164
176
  if err != nil {
165
- return nil, fmt.Errorf("ratio %q 必须是整数枚举值;可参考:%s", ratio, ratioUsage)
177
+ return nil, fmt.Errorf("ratio %q 必须是整数枚举值(如 3 表示 9:16);请用 model describe \"模型名称\" --type image 查询", ratio)
166
178
  }
167
179
  return &value, nil
168
180
  }
@@ -1,6 +1,7 @@
1
1
  package generate_image
2
2
 
3
3
  import (
4
+ "encoding/json"
4
5
  "strings"
5
6
  "testing"
6
7
  )
@@ -19,10 +20,10 @@ func TestValidateOptionsRequiresModel(t *testing.T) {
19
20
  }
20
21
  }
21
22
 
22
- func TestValidateOptionsAllowsServerDecidedModel(t *testing.T) {
23
+ func TestValidateOptionsAllowsDynamicModelName(t *testing.T) {
23
24
  opts := &Options{
24
25
  Prompt: "x",
25
- Model: "seedream_3.0",
26
+ Model: "图片测试模型",
26
27
  }
27
28
 
28
29
  if err := ValidateOptions(opts); err != nil {
@@ -33,7 +34,7 @@ func TestValidateOptionsAllowsServerDecidedModel(t *testing.T) {
33
34
  func TestValidateOptionsAllowsServerDecidedRatio(t *testing.T) {
34
35
  opts := &Options{
35
36
  Prompt: "x",
36
- Model: "seedream_4.5",
37
+ Model: "图片测试模型",
37
38
  Ratio: "99",
38
39
  }
39
40
 
@@ -45,7 +46,7 @@ func TestValidateOptionsAllowsServerDecidedRatio(t *testing.T) {
45
46
  func TestValidateOptionsAllowsServerDecidedResolution(t *testing.T) {
46
47
  opts := &Options{
47
48
  Prompt: "x",
48
- Model: "seedream_4.5",
49
+ Model: "图片测试模型",
49
50
  Resolution: "8K",
50
51
  }
51
52
 
@@ -58,7 +59,7 @@ func TestValidateOptionsRejectsNegativeGenerateImageCount(t *testing.T) {
58
59
  count := -1
59
60
  opts := &Options{
60
61
  Prompt: "x",
61
- Model: "seedream_4.5",
62
+ Model: "图片测试模型",
62
63
  GenerateImageCount: &count,
63
64
  }
64
65
 
@@ -83,6 +84,13 @@ func TestParseRatioSupportsVisibleEnumValues(t *testing.T) {
83
84
  {ratio: "4", want: 4},
84
85
  {ratio: "5", want: 5},
85
86
  {ratio: "6", want: 6},
87
+ {ratio: "7", want: 7},
88
+ {ratio: "8", want: 8},
89
+ {ratio: "9", want: 9},
90
+ {ratio: "10", want: 10},
91
+ {ratio: "11", want: 11},
92
+ {ratio: "12", want: 12},
93
+ {ratio: "99", want: 99},
86
94
  }
87
95
 
88
96
  for _, tt := range cases {
@@ -98,20 +106,55 @@ func TestParseRatioSupportsVisibleEnumValues(t *testing.T) {
98
106
  }
99
107
  }
100
108
 
101
- func TestParseRatioRejectsNonInteger(t *testing.T) {
102
- _, err := parseRatio("1:1")
103
- if err == nil {
104
- t.Fatal("parseRatio() error = nil, want integer validation")
109
+ func TestImageOptionalParametersAndEffortPassThrough(t *testing.T) {
110
+ for _, effort := range []string{"", " HIGH ", "future-effort"} {
111
+ opts := &Options{Prompt: "image", Model: "未来图片模型", Effort: effort}
112
+ if err := ValidateOptions(opts); err != nil {
113
+ t.Fatal(err)
114
+ }
115
+ raw, err := json.Marshal(buildSubmitRunBody(opts, "future-image-model", nil))
116
+ if err != nil {
117
+ t.Fatal(err)
118
+ }
119
+ var body struct {
120
+ Settings map[string]json.RawMessage `json:"general_agent_settings"`
121
+ }
122
+ if err := json.Unmarshal(raw, &body); err != nil {
123
+ t.Fatal(err)
124
+ }
125
+ for _, key := range []string{"ratio", "resolution", "generate_image_count"} {
126
+ if _, exists := body.Settings[key]; exists {
127
+ t.Fatalf("unspecified %s was filled: %s", key, raw)
128
+ }
129
+ }
130
+ if effort == "" {
131
+ if _, exists := body.Settings["image_effort"]; exists {
132
+ t.Fatalf("unspecified effort was filled: %s", raw)
133
+ }
134
+ } else {
135
+ var got string
136
+ if err := json.Unmarshal(body.Settings["image_effort"], &got); err != nil || got != strings.ToLower(strings.TrimSpace(effort)) {
137
+ t.Fatalf("effort not forwarded: %s, %v", raw, err)
138
+ }
139
+ }
105
140
  }
106
- if !strings.Contains(err.Error(), `ratio "1:1" 必须是整数枚举值`) {
107
- t.Fatalf("error = %q, want integer validation", err)
141
+ }
142
+
143
+ func TestParseRatioRejectsNonInteger(t *testing.T) {
144
+ for _, ratio := range []string{"9:16", "adaptive", "17:11", "3.0"} {
145
+ t.Run(ratio, func(t *testing.T) {
146
+ err := ValidateOptions(&Options{Prompt: "image", Model: "未来图片模型", Ratio: ratio})
147
+ if err == nil || !strings.Contains(err.Error(), "必须是整数枚举值") {
148
+ t.Fatalf("ratio %q: error = %v, want integer enum validation", ratio, err)
149
+ }
150
+ })
108
151
  }
109
152
  }
110
153
 
111
154
  func TestValidateOptionsRejectsUnsupportedImageExtension(t *testing.T) {
112
155
  opts := &Options{
113
156
  Prompt: "x",
114
- Model: "seedream_4.5",
157
+ Model: "图片测试模型",
115
158
  ImagePaths: []string{"ref.tiff"},
116
159
  }
117
160
 
@@ -128,13 +171,13 @@ func TestBuildSubmitRunBodyWithGeneralAgentSettings(t *testing.T) {
128
171
  count := 2
129
172
  opts := &Options{
130
173
  Prompt: " 生成小猫海报 ",
131
- Model: " seedream_5.0_pro ",
174
+ Model: " Seedream 5.0 Pro ",
132
175
  Ratio: "6",
133
176
  Resolution: " 4k ",
134
177
  GenerateImageCount: &count,
135
178
  }
136
179
 
137
- body := buildSubmitRunBody(opts, []string{"asset_1"})
180
+ body := buildSubmitRunBody(opts, "seedream_5.0_pro", []string{"asset_1"})
138
181
  if body["agent_name"] != agentNameNest {
139
182
  t.Fatalf("agent_name = %v, want nest agent", body["agent_name"])
140
183
  }
@@ -6,17 +6,15 @@ import (
6
6
  "slices"
7
7
  "strconv"
8
8
  "strings"
9
- )
10
9
 
11
- // Mirrors capcut_business_common.Ratio and the Skill API's skillVideoRatio.
12
- // Custom (1) has no corresponding generate-video --ratio value.
13
- var ratioValues = map[int64]string{
14
- 0: "adaptive", 1: "", 2: "16:9", 3: "9:16", 4: "4:3", 5: "3:4", 6: "1:1",
15
- 7: "2:1", 8: "2.35:1", 9: "1.85:1", 10: "1.125:2.436", 11: "3:2", 12: "2:3", 13: "21:9",
16
- }
10
+ "github.com/Pippit-dev/pippit-cli/internal/common"
11
+ )
17
12
 
18
13
  type dimensionConfig struct {
19
14
  Key string `json:"key"`
15
+ Label string `json:"label"`
16
+ Description string `json:"description"`
17
+ Required *bool `json:"required_field"`
20
18
  DefaultValue *string `json:"default_value"`
21
19
  OptionList []struct {
22
20
  Value string `json:"value"`
@@ -36,6 +34,8 @@ func describeModel(raw json.RawMessage) (json.RawMessage, error) {
36
34
  var fields map[string]json.RawMessage
37
35
  var source struct {
38
36
  Key string `json:"key"`
37
+ Name string `json:"name"`
38
+ Kind string `json:"kind"`
39
39
  Ratios []int64 `json:"supported_ratio_list"`
40
40
  Default *int64 `json:"default_ratio"`
41
41
  Parameter struct {
@@ -57,18 +57,27 @@ func describeModel(raw json.RawMessage) (json.RawMessage, error) {
57
57
  }
58
58
  delete(out, "config_key")
59
59
  delete(out, "is_default") // A server default does not authorize model selection.
60
+ if source.Kind == "image" {
61
+ // Billing maps and other Web-only metadata can contain wire model names.
62
+ // Keep them in the raw catalog, not in host-facing command output.
63
+ out = map[string]any{"name": strings.TrimSpace(source.Name), "kind": source.Kind}
64
+ for _, key := range []string{"description", "parameter_config", "creation_mode_config"} {
65
+ if value, exists := fields[key]; exists {
66
+ out[key] = value
67
+ }
68
+ }
69
+ }
60
70
  warnings := []string{}
61
71
  warn := func(message string) { warnings = append(warnings, message) }
62
- ratios := make([]string, 0, len(source.Ratios))
72
+ ratios := make([]any, 0, len(source.Ratios))
63
73
  for _, value := range source.Ratios {
64
- // Unknown enums and Custom cannot be submitted as CLI ratio strings.
65
- if value := ratioValues[value]; value != "" && !slices.Contains(ratios, value) {
74
+ if value, ok := cliRatioValue(value, source.Kind); ok && !slices.Contains(ratios, value) {
66
75
  ratios = append(ratios, value)
67
76
  }
68
77
  }
69
78
  ratio := map[string]any{"options": ratios}
70
79
  if source.Default != nil {
71
- if value := ratioValues[*source.Default]; value != "" {
80
+ if value, ok := cliRatioValue(*source.Default, source.Kind); ok {
72
81
  if slices.Contains(ratios, value) {
73
82
  ratio["default"] = value
74
83
  } else {
@@ -84,17 +93,37 @@ func describeModel(raw json.RawMessage) (json.RawMessage, error) {
84
93
 
85
94
  seen := make(map[string]bool)
86
95
  for _, dimension := range source.Parameter.Dimensions {
87
- if dimension == nil || (dimension.Key != "resolution" && dimension.Key != "duration") {
96
+ if dimension == nil {
97
+ continue
98
+ }
99
+ if source.Kind == "image" {
100
+ if dimension.Key != "resolution" && dimension.Key != "ratio" && dimension.Key != "effort" {
101
+ continue
102
+ }
103
+ } else if dimension.Key != "resolution" && dimension.Key != "duration" {
88
104
  continue
89
105
  }
90
106
  if seen[dimension.Key] {
91
107
  return nil, fmt.Errorf("模型配置包含重复 %s 维度,请 --refresh 重试", dimension.Key)
92
108
  }
93
109
  seen[dimension.Key] = true
94
- out[dimension.Key] = describeDimension(dimension, warn)
110
+ out[dimension.Key] = describeDimension(dimension, source.Kind, warn)
111
+ }
112
+ if source.Kind == "image" {
113
+ if imageRatio, ok := out["ratio"].(map[string]any); ok {
114
+ if options, ok := imageRatio["options"].([]any); ok {
115
+ labels := make(map[string]string, len(options))
116
+ for _, option := range options {
117
+ value := option.(int64)
118
+ labels[strconv.FormatInt(value, 10)] = common.RatioValue(value)
119
+ }
120
+ imageRatio["option_labels"] = labels
121
+ }
122
+ }
95
123
  }
96
- // Keep unrelated parameter dimensions and combination constraints as supplied.
97
- if parameterRaw, ok := fields["parameter_config"]; ok && string(parameterRaw) != "null" {
124
+ // Image dimensions retain their complete source metadata and combination rules.
125
+ // Video descriptions keep the existing compact representation.
126
+ if parameterRaw, ok := fields["parameter_config"]; source.Kind != "image" && ok && string(parameterRaw) != "null" {
98
127
  var parameter map[string]json.RawMessage
99
128
  if err := json.Unmarshal(parameterRaw, &parameter); err != nil {
100
129
  return nil, err
@@ -146,7 +175,7 @@ func describeModel(raw json.RawMessage) (json.RawMessage, error) {
146
175
  if len(limits) > 0 {
147
176
  out["material_limits"] = limits
148
177
  }
149
- if source.Creation != nil {
178
+ if source.Creation != nil && source.Kind != "image" {
150
179
  modes := make([]map[string]any, 0, len(source.Creation.Modes))
151
180
  for _, rawMode := range source.Creation.Modes {
152
181
  if string(rawMode) == "null" {
@@ -181,7 +210,7 @@ func describeModel(raw json.RawMessage) (json.RawMessage, error) {
181
210
  case "", "inherit":
182
211
  entry["ratio"] = ratio
183
212
  if mode.Key == "text_to_video" && (source.Key == "MiniMax-H3" || source.Key == "MiniMax-H3-Max") {
184
- fixed := make([]string, 0, len(ratios))
213
+ fixed := make([]any, 0, len(ratios))
185
214
  for _, value := range ratios {
186
215
  if value != "adaptive" {
187
216
  fixed = append(fixed, value)
@@ -210,7 +239,7 @@ func describeModel(raw json.RawMessage) (json.RawMessage, error) {
210
239
  }
211
240
  out["creation_modes"] = modes
212
241
  }
213
- if (source.Key == "MiniMax-H3" || source.Key == "MiniMax-H3-Max") && slices.Contains(ratios, "adaptive") {
242
+ if source.Kind != "image" && (source.Key == "MiniMax-H3" || source.Key == "MiniMax-H3-Max") && slices.Contains(ratios, "adaptive") {
214
243
  out["notes"] = []string{"MiniMax 使用 adaptive 需要参考图片或视频;纯文生视频请选择固定比例。创作模式的比例策略优先于模型级选项。"}
215
244
  }
216
245
  if len(warnings) > 0 {
@@ -219,8 +248,29 @@ func describeModel(raw json.RawMessage) (json.RawMessage, error) {
219
248
  return json.Marshal(out)
220
249
  }
221
250
 
222
- func describeDimension(d *dimensionConfig, warn func(string)) map[string]any {
251
+ // Image generation accepts wire enums; video generation accepts ratio strings.
252
+ func cliRatioValue(value int64, kind string) (any, bool) {
253
+ label := common.RatioValue(value)
254
+ if label == "" {
255
+ return nil, false
256
+ }
257
+ if kind == "image" {
258
+ return value, true
259
+ }
260
+ return label, true
261
+ }
262
+
263
+ func describeDimension(d *dimensionConfig, kind string, warn func(string)) map[string]any {
223
264
  out := map[string]any{}
265
+ if d.Label != "" {
266
+ out["label"] = d.Label
267
+ }
268
+ if d.Description != "" {
269
+ out["description"] = d.Description
270
+ }
271
+ if d.Required != nil {
272
+ out["required_field"] = *d.Required
273
+ }
224
274
  if len(d.ActiveWhenAny) > 0 {
225
275
  out["active_when_any"] = d.ActiveWhenAny
226
276
  }
@@ -230,6 +280,16 @@ func describeDimension(d *dimensionConfig, warn func(string)) map[string]any {
230
280
  number, err := strconv.ParseInt(value, 10, 32)
231
281
  return number, err == nil && number > 0
232
282
  }
283
+ if d.Key == "ratio" {
284
+ number, err := strconv.ParseInt(value, 10, 64)
285
+ if err != nil {
286
+ return nil, false
287
+ }
288
+ return cliRatioValue(number, kind)
289
+ }
290
+ if kind == "image" && d.Key == "resolution" {
291
+ return strings.ToUpper(value), value != ""
292
+ }
233
293
  return strings.ToLower(value), value != ""
234
294
  }
235
295
  if d.Key == "duration" {