@pippit-dev/cli 1.0.25 → 1.0.27

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.
Files changed (36) hide show
  1. package/README.md +23 -1
  2. package/checksums.txt +6 -6
  3. package/cmd/generate_image/generate_image.go +1 -0
  4. package/cmd/generate_video/generate_video.go +7 -6
  5. package/cmd/generate_video_test.go +57 -55
  6. package/cmd/model.go +74 -0
  7. package/cmd/model_test.go +81 -0
  8. package/cmd/root.go +1 -0
  9. package/cmd/short_drama/short_drama.go +1 -0
  10. package/cmd/source_test.go +179 -0
  11. package/cmd/submit_run.go +3 -0
  12. package/cmd/update/update.go +1 -1
  13. package/cmd/video_tool/video_tool.go +2 -0
  14. package/dist/checksums.txt +6 -6
  15. package/internal/common/submit_run.go +38 -1
  16. package/internal/config/config.go +13 -10
  17. package/internal/config/config_test.go +3 -0
  18. package/internal/generate_image/generate_image.go +2 -1
  19. package/internal/generate_video/generate_video.go +12 -18
  20. package/internal/generate_video/generate_video_test.go +126 -0
  21. package/internal/models/describe.go +281 -0
  22. package/internal/models/describe_test.go +199 -0
  23. package/internal/models/models.go +220 -0
  24. package/internal/models/models_test.go +200 -0
  25. package/internal/short_drama/submit_run.go +2 -0
  26. package/internal/video_tool/video_tool.go +4 -2
  27. package/package.json +1 -1
  28. package/skills/short-drama/SKILL.md +4 -0
  29. package/skills/xyq-nest-skill/SKILL.md +10 -0
  30. package/skills/xyq-nest-skill/commands/erase-video-subtitle.md +2 -0
  31. package/skills/xyq-nest-skill/commands/generate-image.md +2 -0
  32. package/skills/xyq-nest-skill/commands/generate-video.md +34 -10
  33. package/skills/xyq-nest-skill/commands/model.md +59 -0
  34. package/skills/xyq-nest-skill/commands/video-super-resolution.md +2 -0
  35. package/skills/xyq-nest-skill/scripts/ensure-cli.js +2 -2
  36. package/skills/xyq-nest-skill/scripts/install.md +1 -1
package/README.md CHANGED
@@ -27,6 +27,7 @@
27
27
  | 登录授权 | `status` / `login` / `logout` | [授权](skills/xyq-nest-skill/commands/auth.md) |
28
28
  | 个人 Canvas 画布与节点编辑 | `canvas` | [画布](skills/xyq-nest-skill/commands/canvas.md) |
29
29
  | 生图、参考图编辑 | `generate-image` | [图片](skills/xyq-nest-skill/commands/generate-image.md) |
30
+ | 查看可用视频模型、参数配置 | `model list` / `model describe` | [模型发现](skills/xyq-nest-skill/commands/model.md) |
30
31
  | 生视频、首尾帧 | `generate-video` | [视频](skills/xyq-nest-skill/commands/generate-video.md) |
31
32
  | 视频超分 | `video-super-resolution` | [超分](skills/xyq-nest-skill/commands/video-super-resolution.md) |
32
33
  | 擦字幕 | `erase-video-subtitle` | [擦字幕](skills/xyq-nest-skill/commands/erase-video-subtitle.md) |
@@ -209,6 +210,17 @@ pippit-tool-cli generate-image \
209
210
 
210
211
  图片支持 `.jpg`、`.jpeg`、`.png`、`.gif`、`.bmp`、`.webp`、`.svg`。CLI 会在提交前校验 prompt、model 必填、ratio 整数格式、generate-image-count 非负和文件后缀。
211
212
 
213
+ ## 视频模型发现
214
+
215
+ ```bash
216
+ pippit-tool-cli model list
217
+ pippit-tool-cli model search MiniMax
218
+ pippit-tool-cli model describe MiniMax-H3
219
+ pippit-tool-cli model list --refresh
220
+ ```
221
+
222
+ 使用当前登录凭证查询服务端;成功结果按账号和环境隔离缓存 5 分钟,`--refresh` 强制刷新。失败时提示重试,不回退静态列表或过期缓存。`describe` 将比例枚举转换为可直接传给 `--ratio` 的字符串,并整理分辨率、时长和素材限制;不展示内部 `config_key`,未知比例枚举跳过。生成仍由服务端校验。详见 [模型发现](skills/xyq-nest-skill/commands/model.md)。
223
+
212
224
  ## 生视频 CLI
213
225
 
214
226
  `generate-video` 会上传本地参考图片、视频和音频,然后向视频片段 Agent 提交生视频请求:
@@ -227,7 +239,7 @@ pippit-tool-cli generate-video \
227
239
  --resolution "720p"
228
240
  ```
229
241
 
230
- 命令输出 `thread_id`、`run_id` 和 `web_thread_link`。提交生视频 HTTP 请求时,参考图、参考视频和参考音频会使用上传接口返回的 `pippit_asset_id`,并分别写入 `video_part_tool_param.images`、`video_part_tool_param.videos` 和 `video_part_tool_param.audios`。图片最多 9 张,支持 `.jpg`、`.jpeg`、`.png`、`.gif`、`.bmp`、`.webp`、`.svg`;视频最多 3 个,支持 `.mp4`、`.avi`、`.mov`、`.wmv`、`.flv`、`.webm`、`.mkv`、`.m4v`;音频最多 3 个,仅支持 `.mp3`、`.wav`。普通用户支持模型 `Seedance_2.0_mini_lite`;`seedance2.0_vision`、`seedance2.0_fast_vision`、`Seedance_2.0_mini` `Seedance_2.5` VIP 专属模型。CLI 会在提交前校验 prompt、素材数量和文件后缀;模型、比例、分辨率等语义校验由服务端处理。
242
+ 命令输出 `thread_id`、`run_id` 和 `web_thread_link`。提交生视频 HTTP 请求时,参考图、参考视频和参考音频会使用上传接口返回的 `pippit_asset_id`,并分别写入 `video_part_tool_param.images`、`video_part_tool_param.videos` 和 `video_part_tool_param.audios`。图片支持 `.jpg`、`.jpeg`、`.png`、`.gif`、`.bmp`、`.webp`、`.svg`;视频支持 `.mp4`、`.avi`、`.mov`、`.wmv`、`.flv`、`.webm`、`.mkv`、`.m4v`;音频仅支持 `.mp3`、`.wav`。当前可用模型通过 `model list` 查询,参数详情通过 `model describe MODEL_KEY` 查询。CLI 会在提交前校验 prompt 和文件后缀;模型、比例、分辨率等语义校验由服务端处理。
231
243
 
232
244
  首尾帧生视频时,按首帧、尾帧的顺序传入两次 `--image`,并设置 `--generate-type 1`:
233
245
 
@@ -292,3 +304,13 @@ pippit-tool-cli query-result \
292
304
  原生 CLI 命令通过 `pippit-tool-cli login` 打开小云雀网页授权,并把本机设备专属凭证保存到系统安全凭证库;Access Key 不会显示在终端。可用 `pippit-tool-cli status` 查看状态、`pippit-tool-cli logout` 清除本机登录。
293
305
 
294
306
  CI 或 Agent 可继续显式设置 `XYQ_ACCESS_KEY`,它会覆盖本机网页登录凭证;配置错误时不会静默回退到个人登录。会话提交和查询共享上述凭据。
307
+
308
+ ### 宿主来源统计
309
+
310
+ 调用 Skill 提交接口的命令均支持可选 `--source`:`submit-run`、`generate-image`、`generate-video`、`video-super-resolution`、`erase-video-subtitle`、`short-drama +submit-run`。
311
+
312
+ 由宿主 Agent 根据实际环境静默填写稳定标识,例如豆包办公 `doubao_office`、WorkBuddy `workbuddy`、Codex `codex`。其它来源可使用其真实产品标识;来源未知时省略,不询问用户,也不从 prompt 猜测。该值去掉首尾空白后写入请求顶层 `platform`,仅供统计,不参与创作、模型选择或鉴权;未提供/空值时不发送该字段。不会自动读取环境变量、持久化来源或影响查询、上传、下载、Canvas 命令。
313
+
314
+ ```bash
315
+ pippit-tool-cli generate-video --prompt "小猫在花园散步" --model Seedance_2.0_mini --source workbuddy
316
+ ```
package/checksums.txt CHANGED
@@ -1,6 +1,6 @@
1
- 4a4f33dcfad8ca038e4e17f30a4eb766a593189879570c42c90c67727663b3e3 pippit-tool-cli-1.0.25-darwin-amd64.tar.gz
2
- 239e645a66c08d55b053a27a736afddf6233010166a0f9646420d01eb1104073 pippit-tool-cli-1.0.25-darwin-arm64.tar.gz
3
- 1491e33040ca68be4fafa39505320ee68120742d931fc37f2f3d0774efd143de pippit-tool-cli-1.0.25-linux-amd64.tar.gz
4
- 057c80108de3e61d54be312601577ca2ec40cea32f4f4e6d4d53d3fa5d090439 pippit-tool-cli-1.0.25-linux-arm64.tar.gz
5
- 3eedc462a21392575c6f677cd7d9cceb6028c8b05a39b251e007e5ae09bb74b6 pippit-tool-cli-1.0.25-windows-amd64.zip
6
- 7641c4814e127153e0979a3e4cd69c9bb38870d81940dfa198b2cff7401f3663 pippit-tool-cli-1.0.25-windows-arm64.zip
1
+ 730f01e4c20d9848690579384fb3c635f74d5b0c4938c53a470159eb0798ad56 pippit-tool-cli-1.0.27-darwin-amd64.tar.gz
2
+ 03520884774e9082f5ef070044fa87ff70f18c52fe8d1a4bf4d665509c3af023 pippit-tool-cli-1.0.27-darwin-arm64.tar.gz
3
+ 70348ce883d4223cf6ee0789bd0a72635bfef857d832c54586290eb956a1eb1e pippit-tool-cli-1.0.27-linux-amd64.tar.gz
4
+ d548d7ff144ee723c6420e1d3977c8defbbb92cc19ee86cdb7f021a3812d881c pippit-tool-cli-1.0.27-linux-arm64.tar.gz
5
+ ced13bd604d19987d3d71cb3a8d5518afee64420496dea1b646c19e34efdd23c pippit-tool-cli-1.0.27-windows-amd64.zip
6
+ aeee3a5159843c0b64e453c19a3a7c3dd805f4f0eeafdb08a0e36c604829602f pippit-tool-cli-1.0.27-windows-arm64.zip
@@ -44,6 +44,7 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
44
44
  cmd.SetOut(stdout)
45
45
  cmd.SetErr(stderr)
46
46
  flags := cmd.Flags()
47
+ 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)")
47
48
  flags.StringVar(&opts.Prompt, "prompt", "", "image generation prompt")
48
49
  flags.StringArrayVar(&opts.ImagePaths, "image", nil, "local reference image path; repeat for multiple images")
49
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")
@@ -42,14 +42,15 @@ func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command
42
42
  cmd.SetOut(stdout)
43
43
  cmd.SetErr(stderr)
44
44
  flags := cmd.Flags()
45
+ 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)")
45
46
  flags.StringVar(&opts.Prompt, "prompt", "", "video generation prompt")
46
- flags.StringArrayVar(&opts.ImagePaths, "image", nil, "local reference image path; repeat for multiple images, up to 9")
47
- flags.StringArrayVar(&opts.VideoPaths, "video", nil, "local reference video path; repeat for multiple videos, up to 3")
48
- flags.StringArrayVar(&opts.AudioPaths, "audio", nil, "local reference audio path; repeat for multiple audios, up to 3")
47
+ flags.StringArrayVar(&opts.ImagePaths, "image", nil, "local reference image path; repeat for multiple images")
48
+ flags.StringArrayVar(&opts.VideoPaths, "video", nil, "local reference video path; repeat for multiple videos")
49
+ flags.StringArrayVar(&opts.AudioPaths, "audio", nil, "local reference audio path; repeat for multiple audios")
49
50
  flags.IntVar(&durationSec, "duration", 0, "video duration in seconds")
50
51
  flags.StringVar(&opts.Ratio, "ratio", "", "video ratio, such as 9:16, 16:9, 3:4, 4:3")
51
- flags.StringVar(&opts.Model, "model", "", "video model; normal users: Seedance_2.0_mini_lite; VIP-only: seedance2.0_vision, seedance2.0_fast_vision, Seedance_2.0_mini, Seedance_2.5")
52
- flags.StringVar(&opts.Resolution, "resolution", "", "video resolution, such as 720p, 1080p")
53
- flags.Int64Var(&generateType, "generate-type", 0, "generation type passed to the service; set 1 for first-and-last-frame generation and provide two --image values in first-frame, last-frame order")
52
+ flags.StringVar(&opts.Model, "model", "", "video model key; use 'model list' to discover available models")
53
+ flags.StringVar(&opts.Resolution, "resolution", "", "video resolution; optional for Seedance_2.0_mini and Seedance_2.0_mini_lite (server defaults to 720p); use 'model describe <key>' for current configuration")
54
+ flags.Int64Var(&generateType, "generate-type", 0, "generation type passed to the service; set 1 for first-and-last-frame generation and provide --image values in first-frame, last-frame order; MiniMax and Wan also accept a single first-frame image in this mode")
54
55
  return cmd
55
56
  }
@@ -189,72 +189,74 @@ func TestGenerateVideoRequiresPrompt(t *testing.T) {
189
189
  }
190
190
  }
191
191
 
192
- func TestGenerateVideoRejectsTooManyImages(t *testing.T) {
193
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
194
- t.Fatal("server should not receive request when image count is invalid")
195
- }))
196
- defer server.Close()
197
-
198
- var stdout, stderr bytes.Buffer
199
- root := newTestRootCommand(t, &stdout, &stderr, server.URL)
200
- args := []string{"generate-video", "--prompt", "x"}
201
- for _, path := range mediaPaths("image", ".jpg", 10) {
202
- args = append(args, "--image", path)
203
- }
204
- root.SetArgs(args)
205
-
206
- err := root.Execute()
207
- if err == nil {
208
- t.Fatal("Execute() error = nil, want image count validation")
209
- }
210
- if !strings.Contains(err.Error(), "参考图片最多支持 9 个,当前传入 10 个") {
211
- t.Fatalf("error = %q, want image count validation", err)
212
- }
213
- }
214
-
215
- func TestGenerateVideoRejectsTooManyVideos(t *testing.T) {
216
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
217
- t.Fatal("server should not receive request when video count is invalid")
218
- }))
219
- defer server.Close()
220
-
221
- var stdout, stderr bytes.Buffer
222
- root := newTestRootCommand(t, &stdout, &stderr, server.URL)
192
+ func TestGenerateVideoAcceptsReferencesBeyondFormerLimits(t *testing.T) {
193
+ cwd := chdirTemp(t)
223
194
  args := []string{"generate-video", "--prompt", "x"}
224
- for _, path := range mediaPaths("video", ".mp4", 4) {
225
- args = append(args, "--video", path)
226
- }
227
- root.SetArgs(args)
228
-
229
- err := root.Execute()
230
- if err == nil {
231
- t.Fatal("Execute() error = nil, want video count validation")
232
- }
233
- if !strings.Contains(err.Error(), "参考视频最多支持 3 个,当前传入 4 个") {
234
- t.Fatalf("error = %q, want video count validation", err)
195
+ var assetIDs []string
196
+ for _, media := range []struct {
197
+ flag string
198
+ ext string
199
+ count int
200
+ }{
201
+ {"image", ".jpg", 10},
202
+ {"video", ".mp4", 4},
203
+ {"audio", ".mp3", 4},
204
+ } {
205
+ for _, name := range mediaPaths(media.flag, media.ext, media.count) {
206
+ path := filepath.Join(cwd, name)
207
+ if err := os.WriteFile(path, []byte("media-data"), 0o644); err != nil {
208
+ t.Fatalf("WriteFile(%s): %v", path, err)
209
+ }
210
+ args = append(args, "--"+media.flag, path)
211
+ assetIDs = append(assetIDs, name)
212
+ }
235
213
  }
236
- }
237
214
 
238
- func TestGenerateVideoRejectsTooManyAudios(t *testing.T) {
215
+ uploadIndex := 0
216
+ submitted := false
239
217
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
240
- t.Fatal("server should not receive request when audio count is invalid")
218
+ switch r.URL.Path {
219
+ case "/api/biz/v1/skill/upload_file":
220
+ if uploadIndex >= len(assetIDs) {
221
+ t.Fatalf("unexpected upload %d", uploadIndex)
222
+ }
223
+ _, _ = w.Write([]byte(`{"ret":"0","data":{"pippit_asset_id":"` + assetIDs[uploadIndex] + `"}}`))
224
+ uploadIndex++
225
+ case "/api/biz/v1/skill/submit_run":
226
+ if uploadIndex != len(assetIDs) {
227
+ t.Fatalf("uploaded %d references, want %d", uploadIndex, len(assetIDs))
228
+ }
229
+ data, err := io.ReadAll(r.Body)
230
+ if err != nil {
231
+ t.Fatalf("read body: %v", err)
232
+ }
233
+ var body map[string]any
234
+ if err := sonic.Unmarshal(data, &body); err != nil {
235
+ t.Fatalf("decode body: %v", err)
236
+ }
237
+ param, ok := body["video_part_tool_param"].(map[string]any)
238
+ if !ok {
239
+ t.Fatalf("video_part_tool_param = %#v, want object", body["video_part_tool_param"])
240
+ }
241
+ assertAssetRefs(t, param["images"], assetIDs[:10])
242
+ assertAssetRefs(t, param["videos"], assetIDs[10:14])
243
+ assertAssetRefs(t, param["audios"], assetIDs[14:])
244
+ submitted = true
245
+ _, _ = w.Write([]byte(`{"ret":"0","data":{"run":{"thread_id":"thread_123","run_id":"run_456"}}}`))
246
+ default:
247
+ t.Fatalf("unexpected path %s", r.URL.Path)
248
+ }
241
249
  }))
242
250
  defer server.Close()
243
251
 
244
252
  var stdout, stderr bytes.Buffer
245
253
  root := newTestRootCommand(t, &stdout, &stderr, server.URL)
246
- args := []string{"generate-video", "--prompt", "x"}
247
- for _, path := range mediaPaths("audio", ".mp3", 4) {
248
- args = append(args, "--audio", path)
249
- }
250
254
  root.SetArgs(args)
251
-
252
- err := root.Execute()
253
- if err == nil {
254
- t.Fatal("Execute() error = nil, want audio count validation")
255
+ if err := root.Execute(); err != nil {
256
+ t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
255
257
  }
256
- if !strings.Contains(err.Error(), "参考音频最多支持 3 个,当前传入 4 个") {
257
- t.Fatalf("error = %q, want audio count validation", err)
258
+ if !submitted {
259
+ t.Fatal("generate-video did not submit references")
258
260
  }
259
261
  }
260
262
 
package/cmd/model.go ADDED
@@ -0,0 +1,74 @@
1
+ package cmd
2
+
3
+ import (
4
+ "fmt"
5
+ "io"
6
+ "strings"
7
+ "time"
8
+
9
+ "github.com/Pippit-dev/pippit-cli/internal/common"
10
+ "github.com/Pippit-dev/pippit-cli/internal/models"
11
+ "github.com/spf13/cobra"
12
+ )
13
+
14
+ func newModelCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
15
+ var refresh bool
16
+ var modelType string
17
+ service := models.NewService(runner)
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)
23
+ if err != nil {
24
+ return err
25
+ }
26
+ if result.Warning != "" {
27
+ fmt.Fprintln(stderr, result.Warning)
28
+ }
29
+ output := map[string]any{
30
+ "scene": result.Catalog.Scene,
31
+ "cached": result.Cached, "fetched_at": result.FetchedAt,
32
+ "expires_at": result.FetchedAt.Add(models.CacheTTL).Format(time.RFC3339Nano),
33
+ }
34
+ if describe {
35
+ model, err := result.Catalog.Describe(strings.Join(args, " "))
36
+ if err != nil {
37
+ return err
38
+ }
39
+ output["model"] = model
40
+ } else {
41
+ output["models"] = result.Catalog.Search(strings.Join(args, " "))
42
+ }
43
+ return common.WriteJSON(stdout, output)
44
+ }
45
+ 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.",
49
+ Args: cobra.MaximumNArgs(1),
50
+ RunE: withErrorLog("model", nil, func(cmd *cobra.Command, args []string) error {
51
+ return query(cmd, args, len(args) > 0)
52
+ }),
53
+ }
54
+ cmd.SetOut(stdout)
55
+ cmd.SetErr(stderr)
56
+ 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)")
58
+ cmd.AddCommand(&cobra.Command{
59
+ Use: "list [query]", Aliases: []string{"search"},
60
+ Short: "List video models, optionally filtered by key or name",
61
+ Args: cobra.MaximumNArgs(1),
62
+ RunE: withErrorLog("model list", nil, func(cmd *cobra.Command, args []string) error {
63
+ return query(cmd, args, false)
64
+ }),
65
+ })
66
+ cmd.AddCommand(&cobra.Command{
67
+ Use: "describe <key>", Short: "Show model parameters with CLI-ready ratios, defaults, and limits",
68
+ Args: cobra.ExactArgs(1),
69
+ RunE: withErrorLog("model describe", nil, func(cmd *cobra.Command, args []string) error {
70
+ return query(cmd, args, true)
71
+ }),
72
+ })
73
+ return cmd
74
+ }
@@ -0,0 +1,81 @@
1
+ package cmd
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "net/http"
7
+ "net/http/httptest"
8
+ "strings"
9
+ "testing"
10
+
11
+ "github.com/Pippit-dev/pippit-cli/internal/config"
12
+ )
13
+
14
+ func TestModelCommands(t *testing.T) {
15
+ t.Setenv("HOME", t.TempDir())
16
+ t.Setenv("XDG_CACHE_HOME", t.TempDir())
17
+ t.Setenv("LocalAppData", t.TempDir())
18
+ requests := 0
19
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20
+ requests++
21
+ if r.URL.Path != config.GetAvailableModelListPath {
22
+ t.Errorf("path=%s", r.URL.Path)
23
+ }
24
+ _, _ = w.Write([]byte(`{"ret":"0","data":{"scene":"web_turbo_video_generator","config_key":"key","config":{"models":[{"key":"MiniMax-H3","name":"MiniMax","kind":"video","supported_ratio_list":[0,2,3],"default_ratio":3,"audio_total_limit":0}]}}}`))
25
+ }))
26
+ defer server.Close()
27
+ for i, args := range [][]string{
28
+ {"model", "list"},
29
+ {"model", "search", "MiniMax", "--type", "video"},
30
+ {"model", "describe", "MiniMax-H3"},
31
+ {"model", "MiniMax-H3"},
32
+ {"model", "list", "--refresh"},
33
+ } {
34
+ var stdout, stderr bytes.Buffer
35
+ cfg := config.Load()
36
+ cfg.BaseURL, cfg.AccessKey = server.URL, "model-command-test-key"
37
+ root := newRootCommand(&stdout, &stderr, newRootRunner(cfg))
38
+ root.SetArgs(args)
39
+ if err := root.Execute(); err != nil {
40
+ t.Fatalf("%v: %v", args, err)
41
+ }
42
+ var output map[string]json.RawMessage
43
+ if err := json.Unmarshal(stdout.Bytes(), &output); err != nil {
44
+ t.Fatal(err)
45
+ }
46
+ if _, exists := output["config_key"]; exists {
47
+ t.Fatal("internal config key must not be exposed")
48
+ }
49
+ if i == 2 || i == 3 {
50
+ if bytes.Contains(output["model"], []byte("supported_ratio_list")) || !bytes.Contains(output["model"], []byte(`"default":"9:16"`)) {
51
+ t.Fatalf("ratio must use CLI strings: %s", stdout.String())
52
+ }
53
+ if !bytes.Contains(output["model"], []byte(`"audio_total_limit":0`)) {
54
+ t.Fatalf("missing config: %s", stdout.String())
55
+ }
56
+ } else if !bytes.Contains(output["models"], []byte("MiniMax-H3")) {
57
+ t.Fatalf("missing model: %s", stdout.String())
58
+ }
59
+ want := 1
60
+ if i == 4 {
61
+ want = 2
62
+ }
63
+ if requests != want {
64
+ t.Fatalf("requests=%d want=%d", requests, want)
65
+ }
66
+ }
67
+ }
68
+
69
+ func TestModelHelpDoesNotRequireLoginOrRequest(t *testing.T) {
70
+ for _, args := range [][]string{{"model", "--help"}, {"generate-video", "--help"}} {
71
+ var stdout, stderr bytes.Buffer
72
+ root := NewRootCommand(&stdout, &stderr)
73
+ root.SetArgs(args)
74
+ if err := root.Execute(); err != nil {
75
+ t.Fatal(err)
76
+ }
77
+ if !strings.Contains(stdout.String(), "list") {
78
+ t.Fatalf("help must show discovery: %s", stdout.String())
79
+ }
80
+ }
81
+ }
package/cmd/root.go CHANGED
@@ -72,6 +72,7 @@ func newRootCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Comm
72
72
  root.AddCommand(canvascmd.NewCommand(stdout, stderr, runner))
73
73
  root.AddCommand(newDownloadResultCommand(stdout, stderr, runner))
74
74
  root.AddCommand(newGetCreditBalanceCommand(stdout, stderr, runner))
75
+ root.AddCommand(newModelCommand(stdout, stderr, runner))
75
76
  root.AddCommand(newGetThreadCommand(stdout, stderr, runner))
76
77
  root.AddCommand(newSubmitRunCommand(stdout, stderr, runner))
77
78
  root.AddCommand(newUploadFileCommand(stdout, stderr, runner))
@@ -55,6 +55,7 @@ func newShortDramaSubmitRunCommand(stdout, stderr io.Writer, runner *common.Runn
55
55
  }
56
56
  cmd.SetOut(stdout)
57
57
  cmd.SetErr(stderr)
58
+ cmd.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)")
58
59
  cmd.Flags().StringVar(&opts.Message, "message", "", "message to send to the short drama agent")
59
60
  cmd.Flags().StringVar(&opts.ThreadID, "thread-id", "", "existing thread ID; omit to create a new thread")
60
61
  cmd.Flags().StringArrayVar(&opts.AssetIDs, "asset-ids", nil, "asset ID to attach; repeat for multiple assets")
@@ -0,0 +1,179 @@
1
+ package cmd
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "fmt"
7
+ "net/http"
8
+ "net/http/httptest"
9
+ "os"
10
+ "path/filepath"
11
+ "reflect"
12
+ "strings"
13
+ "testing"
14
+
15
+ "github.com/Pippit-dev/pippit-cli/internal/config"
16
+ )
17
+
18
+ func TestSkillSubmitSourcePreservesCreativeRequest(t *testing.T) {
19
+ for _, name := range []string{"submit-run", "generate-image", "generate-video", "video-super-resolution", "erase-video-subtitle", "short-drama"} {
20
+ t.Run(name, func(t *testing.T) {
21
+ video := filepath.Join(t.TempDir(), "input.mp4")
22
+ if err := os.WriteFile(video, []byte("test-video"), 0600); err != nil {
23
+ t.Fatal(err)
24
+ }
25
+ args := map[string][]string{
26
+ "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"},
28
+ "generate-video": {"generate-video", "--prompt", "cat", "--model", "Seedance_2.0_mini", "--duration", "4", "--ratio", "16:9"},
29
+ "video-super-resolution": {"video-super-resolution", "--video", video, "--output-resolution", "1080p"},
30
+ "erase-video-subtitle": {"erase-video-subtitle", "--video", video},
31
+ "short-drama": {"short-drama", "+submit-run", "--message", "剧本", "--thread-id", "skill_existing", "--asset-ids", "asset_1"},
32
+ }[name]
33
+ var received map[string]any
34
+ var query string
35
+ response := `{"ret":"0","data":{"run":{"thread_id":"thread_1","run_id":"run_1"},"web_thread_link":"https://xyq.example/thread_1"}}`
36
+ submitCount := 0
37
+ uploadFailure := false
38
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
39
+ switch r.URL.Path {
40
+ case config.SubmitRunPath:
41
+ submitCount++
42
+ if r.Method != http.MethodPost || r.Header.Get("Authorization") != "Bearer test-token" {
43
+ t.Error("method or authorization changed")
44
+ }
45
+ if err := json.NewDecoder(r.Body).Decode(&received); err != nil {
46
+ t.Error(err)
47
+ }
48
+ query = r.URL.RawQuery
49
+ _, _ = w.Write([]byte(response))
50
+ case "/api/biz/v1/skill/upload_file":
51
+ if err := r.ParseMultipartForm(1 << 20); err != nil {
52
+ t.Error(err)
53
+ }
54
+ if r.FormValue("platform") != "" || r.FormValue("source") != "" || r.URL.Query().Has("source") || r.URL.Query().Has("platform") {
55
+ t.Error("host attribution leaked into upload")
56
+ }
57
+ if uploadFailure {
58
+ _, _ = w.Write([]byte(`{"ret":"1","errmsg":"upload rejected"}`))
59
+ return
60
+ }
61
+ _, _ = w.Write([]byte(`{"ret":"0","data":{"pippit_asset_id":"video_1"}}`))
62
+ default:
63
+ t.Errorf("unexpected request path: %s", r.URL.Path)
64
+ }
65
+ }))
66
+ defer server.Close()
67
+ execute := func(sourceFlags ...string) string {
68
+ t.Helper()
69
+ received = nil
70
+ var stdout, stderr bytes.Buffer
71
+ root := newTestRootCommand(t, &stdout, &stderr, server.URL)
72
+ commandArgs := append([]string(nil), args...)
73
+ commandArgs = append(commandArgs, sourceFlags...)
74
+ root.SetArgs(commandArgs)
75
+ if err := root.Execute(); err != nil {
76
+ t.Fatal(err)
77
+ }
78
+ if received == nil {
79
+ t.Fatal("no skill submission received")
80
+ }
81
+ return stdout.String()
82
+ }
83
+ wantOutput := execute()
84
+ if _, exists := received["platform"]; exists {
85
+ t.Fatal("missing source must omit platform")
86
+ }
87
+ wantBody, wantQuery := received, query
88
+ for i, tc := range []struct {
89
+ flags []string
90
+ want string
91
+ }{
92
+ {[]string{"--source", "workbuddy"}, "workbuddy"},
93
+ {[]string{"--source", " doubao_office "}, "doubao_office"},
94
+ {[]string{"--source", "codex"}, "codex"},
95
+ {[]string{"--source", "新的宿主"}, "新的宿主"},
96
+ {[]string{"--source", ""}, ""},
97
+ {[]string{"--source", " \t "}, ""},
98
+ {[]string{"--source=workbuddy"}, "workbuddy"},
99
+ {[]string{"--source="}, ""},
100
+ {[]string{"--source=codex", "--source=workbuddy"}, "workbuddy"},
101
+ {[]string{"--source=codex", "--source="}, ""},
102
+ {[]string{"--source", "\u3000codex\u00a0"}, "codex"},
103
+ {[]string{"--source", "host\"&=?/\\测试\nnext"}, "host\"&=?/\\测试\nnext"},
104
+ } {
105
+ t.Run(fmt.Sprintf("source_%02d", i), func(t *testing.T) {
106
+ if got := execute(tc.flags...); got != wantOutput {
107
+ t.Errorf("source changed output: %s", got)
108
+ }
109
+ value, exists := received["platform"]
110
+ if (tc.want == "" && exists) || (tc.want != "" && value != tc.want) {
111
+ t.Errorf("platform = %#v (present=%v), want=%q", value, exists, tc.want)
112
+ }
113
+ delete(received, "platform")
114
+ if !reflect.DeepEqual(received, wantBody) || query != wantQuery {
115
+ t.Errorf("source changed creative request or ROI query: %#v / %s", received, query)
116
+ }
117
+ })
118
+ }
119
+ // A fresh invocation without source must not reuse previous attribution.
120
+ execute()
121
+ if _, exists := received["platform"]; exists {
122
+ t.Error("source leaked into next invocation")
123
+ }
124
+ for _, failure := range []struct{ name, body string }{
125
+ {"business_error", `{"ret":"1","errmsg":"rejected"}`},
126
+ {"invalid_json", `invalid-json`},
127
+ } {
128
+ t.Run(failure.name, func(t *testing.T) {
129
+ response = failure.body
130
+ submitCount = 0
131
+ var stdout, stderr bytes.Buffer
132
+ root := newTestRootCommand(t, &stdout, &stderr, server.URL)
133
+ root.SetArgs(append(append([]string(nil), args...), "--source=codex"))
134
+ if err := root.Execute(); err == nil {
135
+ t.Fatal("source swallowed submit error")
136
+ }
137
+ if submitCount != 1 {
138
+ t.Errorf("submission count = %d, want 1", submitCount)
139
+ }
140
+ if received["platform"] != "codex" {
141
+ t.Error("failed submission lost source")
142
+ }
143
+ })
144
+ }
145
+ if name == "video-super-resolution" || name == "erase-video-subtitle" {
146
+ t.Run("upload_failure", func(t *testing.T) {
147
+ uploadFailure = true
148
+ submitCount = 0
149
+ var stdout, stderr bytes.Buffer
150
+ root := newTestRootCommand(t, &stdout, &stderr, server.URL)
151
+ root.SetArgs(append(append([]string(nil), args...), "--source=workbuddy"))
152
+ if err := root.Execute(); err == nil {
153
+ t.Fatal("source swallowed upload error")
154
+ }
155
+ if submitCount != 0 {
156
+ t.Errorf("submitted %d times after failed upload", submitCount)
157
+ }
158
+ })
159
+ }
160
+ })
161
+ }
162
+ }
163
+
164
+ func TestSourceHelpWithoutCredentials(t *testing.T) {
165
+ for _, args := range [][]string{
166
+ {"submit-run"}, {"generate-image"}, {"generate-video"},
167
+ {"video-super-resolution"}, {"erase-video-subtitle"}, {"short-drama", "+submit-run"},
168
+ } {
169
+ var stdout, stderr bytes.Buffer
170
+ root := newRootCommand(&stdout, &stderr, nil)
171
+ root.SetArgs(append(args, "--help"))
172
+ if err := root.Execute(); err != nil {
173
+ t.Fatal(err)
174
+ }
175
+ if !strings.Contains(stdout.String(), "--source") {
176
+ t.Errorf("help for %v missing --source", args)
177
+ }
178
+ }
179
+ }
package/cmd/submit_run.go CHANGED
@@ -11,6 +11,7 @@ import (
11
11
 
12
12
  func newSubmitRunCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
13
13
  var body struct {
14
+ Platform string `json:"platform,omitempty"`
14
15
  Message string `json:"message"`
15
16
  ThreadID string `json:"thread_id,omitempty"`
16
17
  AssetIDs []string `json:"asset_ids,omitempty"`
@@ -23,6 +24,7 @@ func newSubmitRunCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra
23
24
  if strings.TrimSpace(body.Message) == "" {
24
25
  return fmt.Errorf("缺少必填参数 --message")
25
26
  }
27
+ body.Platform = strings.TrimSpace(body.Platform)
26
28
  // Preserve the original message and identifiers; the backend owns routing.
27
29
  result, err := common.SubmitRun(cmd.Context(), "submit-run", body, runner)
28
30
  if err != nil {
@@ -33,6 +35,7 @@ func newSubmitRunCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra
33
35
  }
34
36
  cmd.SetOut(stdout)
35
37
  cmd.SetErr(stderr)
38
+ cmd.Flags().StringVar(&body.Platform, "source", "", "optional host agent/platform identifier for statistics only; filled silently by the host agent (e.g. doubao_office, workbuddy, codex)")
36
39
  cmd.Flags().StringVar(&body.Message, "message", "", "original message to send (required)")
37
40
  cmd.Flags().StringVar(&body.ThreadID, "thread-id", "", "existing thread ID; omit to create a new thread")
38
41
  cmd.Flags().StringArrayVar(&body.AssetIDs, "asset-ids", nil, "asset ID to attach; repeat for multiple assets")
@@ -63,7 +63,7 @@ func runUpdate(stdout, stderr io.Writer) error {
63
63
  if err != nil {
64
64
  return fmt.Errorf("准备替换当前可执行文件失败: %w", err)
65
65
  }
66
- if err := runInheritEnv(stderr, []string{"PIPPIT_CLI_SKIP_SKILLS=1"}, "npm", "install", "-g", pkg); err != nil {
66
+ if err := runInheritEnv(stderr, []string{"PIPPIT_CLI_SKIP_SKILLS=1"}, "npm", "install", "-g", "--prefer-online", pkg); err != nil {
67
67
  restore()
68
68
  return fmt.Errorf("更新 pippit-tool-cli 失败: %w", err)
69
69
  }
@@ -31,6 +31,7 @@ func NewSuperResolutionCommand(stdout, stderr io.Writer, runner *common.Runner)
31
31
  cmd.SetOut(stdout)
32
32
  cmd.SetErr(stderr)
33
33
  flags := cmd.Flags()
34
+ 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)")
34
35
  flags.StringVar(&opts.VideoPath, "video", "", "local input video path")
35
36
  flags.StringVar(&opts.OutputResolution, "output-resolution", "", "output resolution passed to the service: 720p, 1080p, 2k, or 4k")
36
37
  flags.StringVar(&opts.ToolVersion, "tool-version", "", "optional tool version passed to the service: standard, professional_v1, or professional_v2")
@@ -58,6 +59,7 @@ func NewEraseSubtitleCommand(stdout, stderr io.Writer, runner *common.Runner) *c
58
59
  cmd.SetOut(stdout)
59
60
  cmd.SetErr(stderr)
60
61
  flags := cmd.Flags()
62
+ 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)")
61
63
  flags.StringVar(&opts.VideoPath, "video", "", "local input video path")
62
64
  return cmd
63
65
  }
@@ -1,6 +1,6 @@
1
- 4a4f33dcfad8ca038e4e17f30a4eb766a593189879570c42c90c67727663b3e3 pippit-tool-cli-1.0.25-darwin-amd64.tar.gz
2
- 239e645a66c08d55b053a27a736afddf6233010166a0f9646420d01eb1104073 pippit-tool-cli-1.0.25-darwin-arm64.tar.gz
3
- 1491e33040ca68be4fafa39505320ee68120742d931fc37f2f3d0774efd143de pippit-tool-cli-1.0.25-linux-amd64.tar.gz
4
- 057c80108de3e61d54be312601577ca2ec40cea32f4f4e6d4d53d3fa5d090439 pippit-tool-cli-1.0.25-linux-arm64.tar.gz
5
- 3eedc462a21392575c6f677cd7d9cceb6028c8b05a39b251e007e5ae09bb74b6 pippit-tool-cli-1.0.25-windows-amd64.zip
6
- 7641c4814e127153e0979a3e4cd69c9bb38870d81940dfa198b2cff7401f3663 pippit-tool-cli-1.0.25-windows-arm64.zip
1
+ 730f01e4c20d9848690579384fb3c635f74d5b0c4938c53a470159eb0798ad56 pippit-tool-cli-1.0.27-darwin-amd64.tar.gz
2
+ 03520884774e9082f5ef070044fa87ff70f18c52fe8d1a4bf4d665509c3af023 pippit-tool-cli-1.0.27-darwin-arm64.tar.gz
3
+ 70348ce883d4223cf6ee0789bd0a72635bfef857d832c54586290eb956a1eb1e pippit-tool-cli-1.0.27-linux-amd64.tar.gz
4
+ d548d7ff144ee723c6420e1d3977c8defbbb92cc19ee86cdb7f021a3812d881c pippit-tool-cli-1.0.27-linux-arm64.tar.gz
5
+ ced13bd604d19987d3d71cb3a8d5518afee64420496dea1b646c19e34efdd23c pippit-tool-cli-1.0.27-windows-amd64.zip
6
+ aeee3a5159843c0b64e453c19a3a7c3dd805f4f0eeafdb08a0e36c604829602f pippit-tool-cli-1.0.27-windows-arm64.zip