@pippit-dev/cli 1.0.32 → 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/README.md CHANGED
@@ -51,7 +51,7 @@ node skills/xyq-marketing-skill/scripts/marketing.js query --thread-id THREAD_ID
51
51
  | 登录授权 | `status` / `login` / `logout` | [授权](skills/xyq-nest-skill/commands/auth.md) |
52
52
  | 个人 Canvas 画布与节点编辑 | `canvas` | [画布](skills/xyq-nest-skill/commands/canvas.md) |
53
53
  | 生图、参考图编辑 | `generate-image` | [图片](skills/xyq-nest-skill/commands/generate-image.md) |
54
- | 查看可用视频模型、参数配置 | `model list` / `model describe` | [模型发现](skills/xyq-nest-skill/commands/model.md) |
54
+ | 查看可用图片/视频模型、参数配置 | `model list` / `model describe` | [模型发现](skills/xyq-nest-skill/commands/model.md) |
55
55
  | 生视频、首尾帧 | `generate-video` | [视频](skills/xyq-nest-skill/commands/generate-video.md) |
56
56
  | 视频超分 | `video-super-resolution` | [超分](skills/xyq-nest-skill/commands/video-super-resolution.md) |
57
57
  | 擦字幕 | `erase-video-subtitle` | [擦字幕](skills/xyq-nest-skill/commands/erase-video-subtitle.md) |
@@ -205,20 +205,22 @@ pippit-tool-cli canvas command run xyq.generation.update_prompt \
205
205
 
206
206
  ## 生图 CLI
207
207
 
208
- `generate-image` 会上传本地参考图片,然后向综合 Nest Agent 提交生图请求:
208
+ `generate-image` 使用图片模型的展示名称;先查询当前列表并选择模型,再上传本地参考图片并提交生图请求。以下假设列表包含该名称:
209
209
 
210
210
  ```bash
211
211
  pippit-tool-cli generate-image \
212
212
  --prompt "生成一张小猫海报" \
213
213
  --image "~/images/cat.png" \
214
- --model "seedream_4.5" \
214
+ --model "智能图片V2.5 Fast" \
215
215
  --ratio 6 \
216
216
  --generate-image-count 2
217
217
  ```
218
218
 
219
- 命令输出 `thread_id`、`run_id` 和 `web_thread_link`。提交 HTTP 请求时,`agent_name` 固定为 `pippit_nest_agent`,参考图会使用上传接口返回的 `pippit_asset_id` 写入顶层 `asset_ids`,生图模型写入 `general_agent_settings.image_model`,比例写入 `general_agent_settings.ratio`,生图数量写入 `general_agent_settings.generate_image_count`。`--model` 为必填参数,CLI 只做非空校验,具体模型值是否可用由服务端决定。
219
+ 命令输出 `thread_id`、`run_id` 和 `web_thread_link`。提交 HTTP 请求时,`agent_name` 固定为 `pippit_nest_agent`,参考图会使用上传接口返回的 `pippit_asset_id` 写入顶层 `asset_ids`,生图模型写入 `general_agent_settings.image_model`,比例写入 `general_agent_settings.ratio`,生图数量写入 `general_agent_settings.generate_image_count`。`--model` 为必填参数,填写 `model list --type image` 返回的完整名称,例如 `"智能图片V2.5 Fast"`。CLI 在上传素材前查询或复用有效缓存,解析名称后仅在提交请求中填写服务端模型标识;名称缺失、重复或未找到时停止,不猜测模型。
220
220
 
221
- `--ratio` 可选,填写服务端 `Ratio` 枚举值。CLI 只做整数格式解析,不检查枚举值是否在下表范围内;具体值是否可用由服务端决定。常用枚举值含义如下:
221
+ 图片 `model list/search/describe` 返回给宿主的是 API 下发的展示名称 `name`,例如 `美学模型 8.2`。生成时填写 `--model "美学模型 8.2"`,CLI 仅在提交请求中使用该条目对应的 `key`。名称、可用模型和参数均由接口动态提供,不维护静态名称映射。
222
+
223
+ `--ratio` 可选,只接受服务端 `Ratio` 数字枚举,例如 `--ratio 3` 表示 `9:16`。通过 `model describe "模型名称" --type image` 查看当前可用的数字 `options/default`,`option_labels` 说明每个数字对应的比例。CLI 校验整数格式,模型是否支持该枚举由服务端决定。常用枚举值含义如下:
222
224
 
223
225
  | ratio 参数 | IDL 枚举 | 含义 |
224
226
  | ---: | --- | --- |
@@ -232,18 +234,27 @@ pippit-tool-cli generate-image \
232
234
 
233
235
  `--generate-image-count` 可选,填写生图数量,对应 IDL 字段 `GeneralSettingsPart.GenerateImageCount` / JSON 字段 `generate_image_count`。CLI 只校验不能为负数;具体数量范围由服务端决定。
234
236
 
235
- 图片支持 `.jpg`、`.jpeg`、`.png`、`.gif`、`.bmp`、`.webp`、`.svg`。CLI 会在提交前校验 prompt、model 必填、ratio 整数格式、generate-image-count 非负和文件后缀。
237
+ `--resolution` 写入 `general_agent_settings.resolution`(转大写),新增 `--effort` 写入 `general_agent_settings.image_effort`(转小写)。分辨率和推理强度选项都来自该模型的动态配置;没有 `effort` 维度的模型不展示推理强度选择。只传用户指定的参数,未指定时省略,不自动补查询默认值。
238
+
239
+ 图片支持 `.jpg`、`.jpeg`、`.png`、`.gif`、`.bmp`、`.webp`、`.svg`。CLI 会在提交前校验 prompt、model 必填、ratio 格式、generate-image-count 非负和文件后缀;不新增模型或参数组合白名单。
236
240
 
237
- ## 视频模型发现
241
+ ## 图片与视频模型发现
238
242
 
239
243
  ```bash
244
+ # 图片模型及可选参数
245
+ pippit-tool-cli model list --type image
246
+ pippit-tool-cli model search "智能图片" --type image
247
+ pippit-tool-cli model describe "智能图片V2.5 Fast" --type image
248
+ pippit-tool-cli model list --type image --refresh
249
+
250
+ # 省略 --type 保持查询视频
240
251
  pippit-tool-cli model list
241
252
  pippit-tool-cli model search MiniMax
242
253
  pippit-tool-cli model describe MiniMax-H3
243
254
  pippit-tool-cli model list --refresh
244
255
  ```
245
256
 
246
- 使用当前登录凭证查询服务端;成功结果按账号和环境隔离缓存 5 分钟,`--refresh` 强制刷新。失败时提示重试,不回退静态列表或过期缓存。`describe` 将比例枚举转换为可直接传给 `--ratio` 的字符串,并整理分辨率、时长和素材限制;不展示内部 `config_key`,未知比例枚举跳过。生成仍由服务端校验。详见 [模型发现](skills/xyq-nest-skill/commands/model.md)。
257
+ 使用当前个人登录凭证查询服务端 Skill 模型接口;图片 scene 为 `web_image_agent`,视频为 `web_turbo_video_generator`,不传 TeamID。成功结果按账号、环境和场景隔离缓存 5 分钟,`--refresh` 强制刷新。失败时提示重试,不回退静态列表或过期缓存。`describe` 提供可直接传给生成命令的参数:图片比例保留数字枚举并附比例说明,视频比例转换为字符串,同时整理分辨率、图片推理强度、视频时长和素材限制;不展示内部 `config_key`,未知比例枚举跳过。图片列表和详情仅用 `name` 标识模型,不展示底层模型枚举;搜索、详情查询和 `generate-image --model` 均使用名称,包含空格时加引号。图片详情保留原始 `parameter_config`,包括条件、必选标记、选项说明和参数组合约束。生成仍由服务端校验;服务端尚未开放图片场景或提交参数时会返回错误,不回退静态模型清单。详见 [模型发现](skills/xyq-nest-skill/commands/model.md)。
247
258
 
248
259
  ## 生视频 CLI
249
260
 
package/checksums.txt CHANGED
@@ -1,6 +1,6 @@
1
- 2f3c2a486a66e408a6d3841f19ecb909fbaa657636a469ee782a424e59596a68 pippit-tool-cli-1.0.32-darwin-amd64.tar.gz
2
- f4ccd97a007167a9677ea85be52339a041f5448c4e4afd2ec996d32006ccec83 pippit-tool-cli-1.0.32-darwin-arm64.tar.gz
3
- 91ae74eb2067844be741aa8a5f5f932152214c168d43efae801244d46e3f5cab pippit-tool-cli-1.0.32-linux-amd64.tar.gz
4
- 1928d81ea0f11fa20ae65a689654a9d2dddc4cacb4e6a5ae41a5cdd35e40eac5 pippit-tool-cli-1.0.32-linux-arm64.tar.gz
5
- 79283336e84c3196454aecf96c367383795636f4e153234a11996124a78dd7a5 pippit-tool-cli-1.0.32-windows-amd64.zip
6
- aa73d963a3dd7d0eb5dba433fdf14d97510dfbe90515aab5318e55c0147d14d9 pippit-tool-cli-1.0.32-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
@@ -34,6 +34,7 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
34
34
  "model": strings.TrimSpace(opts.Model),
35
35
  "ratio": strings.TrimSpace(opts.Ratio),
36
36
  "resolution": strings.ToUpper(strings.TrimSpace(opts.Resolution)),
37
+ "effort": strings.ToLower(strings.TrimSpace(opts.Effort)),
37
38
  "generate_image_count": optionalIntString(opts.GenerateImageCount),
38
39
  })
39
40
  return err
@@ -47,9 +48,10 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
47
48
  flags.StringVar(&opts.Source, "source", "", "optional host agent/platform identifier for statistics only; filled silently by the host agent (e.g. doubao_office, workbuddy, codex)")
48
49
  flags.StringVar(&opts.Prompt, "prompt", "", "image generation prompt")
49
50
  flags.StringArrayVar(&opts.ImagePaths, "image", nil, "local reference image path; repeat for multiple images")
50
- flags.StringVar(&opts.Model, "model", "", "image model; supported: seedream_5.0_pro, seedream_5.0, seedream_4.3, nova2, seedream_4.5, seedream_4.1, seedream_4")
51
+ flags.StringVar(&opts.Model, "model", "", "exact image model name from 'model list --type image' (quote names containing spaces)")
51
52
  flags.StringVar(&opts.Ratio, "ratio", "", "image ratio; "+internalgen.SupportedRatioUsage())
52
- flags.StringVar(&opts.Resolution, "resolution", "", "image resolution; only seedream_5.0_pro supports these options: 1K, 2K, 4K")
53
+ flags.StringVar(&opts.Resolution, "resolution", "", "image resolution; use 'model describe \"MODEL_NAME\" --type image' for available values")
54
+ flags.StringVar(&opts.Effort, "effort", "", "image reasoning effort; only use values exposed by 'model describe \"MODEL_NAME\" --type image'")
53
55
  flags.IntVar(&generateImageCount, "generate-image-count", 0, "generated image count")
54
56
  return cmd
55
57
  }
@@ -2,6 +2,7 @@ package cmd
2
2
 
3
3
  import (
4
4
  "bytes"
5
+ "encoding/json"
5
6
  "io"
6
7
  "net/http"
7
8
  "net/http/httptest"
@@ -10,18 +11,29 @@ import (
10
11
  "strings"
11
12
  "testing"
12
13
 
14
+ "github.com/Pippit-dev/pippit-cli/internal/config"
13
15
  "github.com/bytedance/sonic"
14
16
  )
15
17
 
16
18
  func TestGenerateImage(t *testing.T) {
19
+ t.Setenv("HOME", t.TempDir())
20
+ t.Setenv("XDG_CACHE_HOME", t.TempDir())
21
+ t.Setenv("LocalAppData", t.TempDir())
17
22
  var uploaded bool
23
+ var modelQueried bool
18
24
 
19
25
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20
26
  if r.Header.Get("Authorization") != "Bearer test-token" {
21
27
  t.Fatalf("Authorization = %q, want test bearer token", r.Header.Get("Authorization"))
22
28
  }
23
29
  switch r.URL.Path {
30
+ case "/api/biz/v1/skill/get_available_model_list":
31
+ modelQueried = true
32
+ _, _ = 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"}]}}}`))
24
33
  case "/api/biz/v1/skill/upload_file":
34
+ if !modelQueried {
35
+ t.Fatal("image name must be resolved before uploading")
36
+ }
25
37
  if r.Method != http.MethodPost {
26
38
  t.Fatalf("upload method = %s, want POST", r.Method)
27
39
  }
@@ -100,7 +112,7 @@ func TestGenerateImage(t *testing.T) {
100
112
  "generate-image",
101
113
  "--prompt", "生成小猫海报",
102
114
  "--image", image,
103
- "--model", "seedream_5.0_pro",
115
+ "--model", "Seedream 5.0 Pro",
104
116
  "--ratio", "6",
105
117
  "--resolution", "4K",
106
118
  "--generate-image-count", "2",
@@ -137,3 +149,140 @@ func TestGenerateImageRequiresModel(t *testing.T) {
137
149
  t.Fatalf("error = %q, want model validation", err)
138
150
  }
139
151
  }
152
+
153
+ func TestGenerateImageDynamicParameters(t *testing.T) {
154
+ t.Setenv("HOME", t.TempDir())
155
+ t.Setenv("XDG_CACHE_HOME", t.TempDir())
156
+ t.Setenv("LocalAppData", t.TempDir())
157
+ requests := 0
158
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
159
+ requests++
160
+ if r.URL.Path == "/api/biz/v1/skill/get_available_model_list" {
161
+ _, _ = w.Write([]byte(`{"ret":"0","data":{"scene":"web_image_agent","config_key":"image-key","config":{"models":[{"key":"future-image-model","name":"未来图片模型","kind":"image"}]}}}`))
162
+ return
163
+ }
164
+ if r.URL.Path != "/api/biz/v1/skill/submit_run" {
165
+ t.Errorf("unexpected path: %s", r.URL.Path)
166
+ }
167
+ raw, err := io.ReadAll(r.Body)
168
+ if err != nil {
169
+ t.Error(err)
170
+ return
171
+ }
172
+ var body struct {
173
+ AgentName string `json:"agent_name"`
174
+ Settings struct {
175
+ Model string `json:"image_model"`
176
+ Ratio int `json:"ratio"`
177
+ Resolution string `json:"resolution"`
178
+ Effort string `json:"image_effort"`
179
+ } `json:"general_agent_settings"`
180
+ }
181
+ if err := sonic.Unmarshal(raw, &body); err != nil {
182
+ t.Error(err)
183
+ return
184
+ }
185
+ if body.AgentName != "pippit_nest_agent" || body.Settings.Model != "future-image-model" || body.Settings.Ratio != 13 || body.Settings.Resolution != "8K" || body.Settings.Effort != "future-effort" {
186
+ t.Errorf("incorrect request mapping: %s", raw)
187
+ }
188
+ _, _ = w.Write([]byte(`{"ret":"0","data":{"run":{"thread_id":"image-thread","run_id":"image-run"}}}`))
189
+ }))
190
+ defer server.Close()
191
+ var stdout, stderr bytes.Buffer
192
+ root := newTestRootCommand(t, &stdout, &stderr, server.URL)
193
+ root.SetArgs([]string{"generate-image", "--prompt", "image", "--model", "未来图片模型", "--ratio", "13", "--resolution", "8k", "--effort", " FUTURE-EFFORT "})
194
+ if err := root.Execute(); err != nil || requests != 2 {
195
+ t.Fatalf("Execute: err=%v requests=%d", err, requests)
196
+ }
197
+ }
198
+
199
+ func TestImageModelNameFromDiscoveryToSubmission(t *testing.T) {
200
+ t.Setenv("HOME", t.TempDir())
201
+ t.Setenv("XDG_CACHE_HOME", t.TempDir())
202
+ t.Setenv("LocalAppData", t.TempDir())
203
+ queries, submits := 0, 0
204
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
205
+ switch r.URL.Path {
206
+ case config.GetAvailableModelListPath:
207
+ queries++
208
+ _, _ = w.Write([]byte(`{"ret":"0","data":{"scene":"web_image_agent","config_key":"image-key","config":{"models":[{"key":"ali_midjourney_8_2","name":"美学模型 8.2","benefit_map_by_combination":{"ali_midjourney_8_2":{"resource_id":"ali_midjourney_8_2"}},"kind":"image"}]}}}`))
209
+ case config.SubmitRunPath:
210
+ submits++
211
+ var body struct {
212
+ Settings map[string]json.RawMessage `json:"general_agent_settings"`
213
+ }
214
+ if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
215
+ t.Error(err)
216
+ }
217
+ if string(body.Settings["image_model"]) != `"ali_midjourney_8_2"` || len(body.Settings) != 1 {
218
+ t.Errorf("wire model not resolved or unrequested defaults filled: %v", body.Settings)
219
+ }
220
+ _, _ = w.Write([]byte(`{"ret":"1","errmsg":"ali_midjourney_8_2 暂时不可用","log_id":"image-log"}`))
221
+ default:
222
+ t.Errorf("unexpected request: %s", r.URL.Path)
223
+ }
224
+ }))
225
+ defer server.Close()
226
+ execute := func(args ...string) (string, string, error) {
227
+ var stdout, stderr bytes.Buffer
228
+ root := newTestRootCommand(t, &stdout, &stderr, server.URL)
229
+ root.SetArgs(args)
230
+ err := root.Execute()
231
+ return stdout.String(), stderr.String(), err
232
+ }
233
+ list, _, err := execute("model", "list", "--type", "image")
234
+ if err != nil {
235
+ t.Fatal(err)
236
+ }
237
+ var output struct{ Models []struct{ Name string } }
238
+ if err := json.Unmarshal([]byte(list), &output); err != nil || len(output.Models) != 1 {
239
+ t.Fatalf("list=%s err=%v", list, err)
240
+ }
241
+ name := output.Models[0].Name
242
+ if name != "美学模型 8.2" {
243
+ t.Fatalf("unexpected display name: %q", name)
244
+ }
245
+ detail, _, err := execute("model", "describe", name, "--type", "image")
246
+ if err != nil || !strings.Contains(detail, name) || strings.Contains(list+detail, "ali_midjourney_8_2") {
247
+ t.Fatalf("user-facing identity changed: list=%s detail=%s err=%v", list, detail, err)
248
+ }
249
+ stdout, stderr, err := execute("generate-image", "--prompt", "一张猫咪图片", "--model", name)
250
+ if err == nil || !strings.Contains(err.Error(), name) || !strings.Contains(err.Error(), "image-log") || strings.Contains(stdout+stderr+err.Error(), "ali_midjourney_8_2") {
251
+ t.Fatalf("server rejection must keep name and log ID: stdout=%s stderr=%s err=%v", stdout, stderr, err)
252
+ }
253
+ if queries != 1 || submits != 1 {
254
+ t.Fatalf("expected shared cache and no submit retry: queries=%d submits=%d", queries, submits)
255
+ }
256
+ }
257
+
258
+ func TestImageNameFailureStopsBeforeUploadAndSubmit(t *testing.T) {
259
+ for _, tc := range []struct{ name, selection, models string }{
260
+ {"unknown name", "不存在", `[{"key":"private-image","name":"图片模型","kind":"image"}]`},
261
+ {"wire key", "private-image", `[{"key":"private-image","name":"图片模型","kind":"image"}]`},
262
+ {"missing name", "图片模型", `[{"key":"private-image","kind":"image"}]`},
263
+ {"duplicate name", "图片模型", `[{"key":"private-image","name":"图片模型","kind":"image"},{"key":"private-image-2","name":"图片模型","kind":"image"}]`},
264
+ {"empty list", "图片模型", `[]`},
265
+ } {
266
+ t.Run(tc.name, func(t *testing.T) {
267
+ t.Setenv("HOME", t.TempDir())
268
+ t.Setenv("XDG_CACHE_HOME", t.TempDir())
269
+ t.Setenv("LocalAppData", t.TempDir())
270
+ requests := 0
271
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
272
+ requests++
273
+ if r.URL.Path != config.GetAvailableModelListPath {
274
+ t.Errorf("must not upload or submit: %s", r.URL.Path)
275
+ }
276
+ _, _ = w.Write([]byte(`{"ret":"0","data":{"scene":"web_image_agent","config_key":"image-key","config":{"models":` + tc.models + `}}}`))
277
+ }))
278
+ defer server.Close()
279
+ var stdout, stderr bytes.Buffer
280
+ root := newTestRootCommand(t, &stdout, &stderr, server.URL)
281
+ root.SetArgs([]string{"generate-image", "--prompt", "image", "--model", tc.selection, "--image", "/missing.png"})
282
+ err := root.Execute()
283
+ if err == nil || requests != 1 || strings.Contains(stdout.String()+stderr.String()+err.Error(), "private-image") {
284
+ t.Fatalf("invalid name accepted or exposed: requests=%d stdout=%s stderr=%s err=%v", requests, stdout.String(), stderr.String(), err)
285
+ }
286
+ })
287
+ }
288
+ }
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
+ }
@@ -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
- 2f3c2a486a66e408a6d3841f19ecb909fbaa657636a469ee782a424e59596a68 pippit-tool-cli-1.0.32-darwin-amd64.tar.gz
2
- f4ccd97a007167a9677ea85be52339a041f5448c4e4afd2ec996d32006ccec83 pippit-tool-cli-1.0.32-darwin-arm64.tar.gz
3
- 91ae74eb2067844be741aa8a5f5f932152214c168d43efae801244d46e3f5cab pippit-tool-cli-1.0.32-linux-amd64.tar.gz
4
- 1928d81ea0f11fa20ae65a689654a9d2dddc4cacb4e6a5ae41a5cdd35e40eac5 pippit-tool-cli-1.0.32-linux-arm64.tar.gz
5
- 79283336e84c3196454aecf96c367383795636f4e153234a11996124a78dd7a5 pippit-tool-cli-1.0.32-windows-amd64.zip
6
- aa73d963a3dd7d0eb5dba433fdf14d97510dfbe90515aab5318e55c0147d14d9 pippit-tool-cli-1.0.32-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
@@ -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
  }