@pippit-dev/cli 1.0.26 → 1.0.28

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 +4 -3
  5. package/cmd/model.go +74 -0
  6. package/cmd/model_test.go +81 -0
  7. package/cmd/root.go +1 -0
  8. package/cmd/short_drama/short_drama.go +1 -0
  9. package/cmd/source_test.go +179 -0
  10. package/cmd/submit_run.go +3 -0
  11. package/cmd/update/update.go +1 -1
  12. package/cmd/video_tool/video_tool.go +2 -0
  13. package/dist/checksums.txt +6 -6
  14. package/internal/common/submit_run.go +38 -1
  15. package/internal/config/config.go +13 -10
  16. package/internal/config/config_test.go +3 -0
  17. package/internal/generate_image/generate_image.go +2 -1
  18. package/internal/generate_video/generate_video.go +12 -3
  19. package/internal/generate_video/generate_video_test.go +126 -0
  20. package/internal/models/describe.go +281 -0
  21. package/internal/models/describe_test.go +199 -0
  22. package/internal/models/models.go +220 -0
  23. package/internal/models/models_test.go +200 -0
  24. package/internal/short_drama/submit_run.go +2 -0
  25. package/internal/video_tool/video_tool.go +4 -2
  26. package/package.json +1 -1
  27. package/scripts/install.js +3 -2
  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 +31 -7
  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 +3 -3
  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`。图片支持 `.jpg`、`.jpeg`、`.png`、`.gif`、`.bmp`、`.webp`、`.svg`;视频支持 `.mp4`、`.avi`、`.mov`、`.wmv`、`.flv`、`.webm`、`.mkv`、`.m4v`;音频仅支持 `.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
- 485d9e51092beaf675a872f890fd744460e7afe45b27ec500ab6b5c90ebb6d0c pippit-tool-cli-1.0.26-darwin-amd64.tar.gz
2
- 978cca38261d539ea8ce2bcfd15deb24b4b317f38c084fdde92a5b9ca51f0739 pippit-tool-cli-1.0.26-darwin-arm64.tar.gz
3
- d772c3fcef2c7b7ff0e6e52d6f10b2ee4cff1cd5fbdff9f08e0bd252ac378e79 pippit-tool-cli-1.0.26-linux-amd64.tar.gz
4
- b16e7f2e7d9bbfe122eb0895ae6769a00d230ca250717aebb3f9b8eea0c04eae pippit-tool-cli-1.0.26-linux-arm64.tar.gz
5
- fd05b98a6001602efad98731a4777cb035eea2bea2acbccb2c1d02af0223176e pippit-tool-cli-1.0.26-windows-amd64.zip
6
- a0c873456cf254b0e452c6e376efec5a425ef044b3a1a671d063fa5a613242d0 pippit-tool-cli-1.0.26-windows-arm64.zip
1
+ 4b34accaf9d10cd786f1ac962434106363633e7353852a82df20166e761c7a4e pippit-tool-cli-1.0.28-darwin-amd64.tar.gz
2
+ 9250230123f5db4d78b91644b5f7346cb6be002a42144df3870d631f16183ea4 pippit-tool-cli-1.0.28-darwin-arm64.tar.gz
3
+ 1ebcb2642dd56802556180ab2cea5a963df6a064fead3e06056755249a86810b pippit-tool-cli-1.0.28-linux-amd64.tar.gz
4
+ c80e22e65df2a8f1aa90c6dbf9c47b3ca2c4e58367a373e9a28a4133f2d12cb5 pippit-tool-cli-1.0.28-linux-arm64.tar.gz
5
+ 03fc5d241e002860e05eaf39be5c3cd18c741539fec5bb3ea37c25941c83119f pippit-tool-cli-1.0.28-windows-amd64.zip
6
+ 0c5adce56ee08df04082c8f121be968145e3db41c3b53e03e23b5593fd77f315 pippit-tool-cli-1.0.28-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
47
  flags.StringArrayVar(&opts.ImagePaths, "image", nil, "local reference image path; repeat for multiple images")
47
48
  flags.StringArrayVar(&opts.VideoPaths, "video", nil, "local reference video path; repeat for multiple videos")
48
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
  }
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", "--foreground-scripts", 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
- 485d9e51092beaf675a872f890fd744460e7afe45b27ec500ab6b5c90ebb6d0c pippit-tool-cli-1.0.26-darwin-amd64.tar.gz
2
- 978cca38261d539ea8ce2bcfd15deb24b4b317f38c084fdde92a5b9ca51f0739 pippit-tool-cli-1.0.26-darwin-arm64.tar.gz
3
- d772c3fcef2c7b7ff0e6e52d6f10b2ee4cff1cd5fbdff9f08e0bd252ac378e79 pippit-tool-cli-1.0.26-linux-amd64.tar.gz
4
- b16e7f2e7d9bbfe122eb0895ae6769a00d230ca250717aebb3f9b8eea0c04eae pippit-tool-cli-1.0.26-linux-arm64.tar.gz
5
- fd05b98a6001602efad98731a4777cb035eea2bea2acbccb2c1d02af0223176e pippit-tool-cli-1.0.26-windows-amd64.zip
6
- a0c873456cf254b0e452c6e376efec5a425ef044b3a1a671d063fa5a613242d0 pippit-tool-cli-1.0.26-windows-arm64.zip
1
+ 4b34accaf9d10cd786f1ac962434106363633e7353852a82df20166e761c7a4e pippit-tool-cli-1.0.28-darwin-amd64.tar.gz
2
+ 9250230123f5db4d78b91644b5f7346cb6be002a42144df3870d631f16183ea4 pippit-tool-cli-1.0.28-darwin-arm64.tar.gz
3
+ 1ebcb2642dd56802556180ab2cea5a963df6a064fead3e06056755249a86810b pippit-tool-cli-1.0.28-linux-amd64.tar.gz
4
+ c80e22e65df2a8f1aa90c6dbf9c47b3ca2c4e58367a373e9a28a4133f2d12cb5 pippit-tool-cli-1.0.28-linux-arm64.tar.gz
5
+ 03fc5d241e002860e05eaf39be5c3cd18c741539fec5bb3ea37c25941c83119f pippit-tool-cli-1.0.28-windows-amd64.zip
6
+ 0c5adce56ee08df04082c8f121be968145e3db41c3b53e03e23b5593fd77f315 pippit-tool-cli-1.0.28-windows-arm64.zip
@@ -2,7 +2,10 @@ package common
2
2
 
3
3
  import (
4
4
  "context"
5
+ "encoding/json"
5
6
  "fmt"
7
+ "net/url"
8
+ "strings"
6
9
 
7
10
  "github.com/Pippit-dev/pippit-cli/internal/config"
8
11
  )
@@ -37,11 +40,36 @@ type SubmitRunResponseRun struct {
37
40
 
38
41
  // SubmitRun sends a submit_run request and validates its shared response envelope.
39
42
  func SubmitRun(ctx context.Context, command string, body any, runner *Runner) (*SubmitRunResult, error) {
43
+ return submitRun(ctx, command, SubmitRunPath(runner), body, runner)
44
+ }
45
+
46
+ // SubmitRunWithBabiParam carries client attribution in the same query field as Web.
47
+ func SubmitRunWithBabiParam(ctx context.Context, command string, body any, runner *Runner, babiParam map[string]string) (*SubmitRunResult, error) {
48
+ path, err := url.Parse(SubmitRunPath(runner))
49
+ if err != nil {
50
+ return nil, fmt.Errorf("解析 submit_run 路径失败: %w", err)
51
+ }
52
+ query, err := url.ParseQuery(path.RawQuery)
53
+ if err != nil {
54
+ return nil, fmt.Errorf("解析 submit_run 查询参数失败: %w", err)
55
+ }
56
+ raw, err := json.Marshal(babiParam)
57
+ if err != nil {
58
+ return nil, fmt.Errorf("序列化 babi_param 失败: %w", err)
59
+ }
60
+ if !query.Has("babi_param") {
61
+ query.Set("babi_param", string(raw))
62
+ }
63
+ path.RawQuery = query.Encode()
64
+ return submitRun(ctx, command, path.String(), body, runner)
65
+ }
66
+
67
+ func submitRun(ctx context.Context, command, path string, body any, runner *Runner) (*SubmitRunResult, error) {
40
68
  if runner == nil || runner.Client == nil {
41
69
  return nil, fmt.Errorf("%s 运行器客户端缺失", command)
42
70
  }
43
71
  var resp SubmitRunResponse
44
- if err := runner.Client.SendRequest(ctx, SubmitRunPath(runner), body, &resp); err != nil {
72
+ if err := runner.Client.SendRequest(ctx, path, body, &resp); err != nil {
45
73
  return nil, fmt.Errorf("提交 %s 请求失败: %w", command, err)
46
74
  }
47
75
  if resp.Ret != "0" {
@@ -71,3 +99,12 @@ func SubmitRunPath(runner *Runner) string {
71
99
  }
72
100
  return config.SubmitRunPath
73
101
  }
102
+
103
+ // WithSubmitRunSource adds optional host attribution to a newly constructed skill request.
104
+ // The API consumes this metadata as platform; it is not part of the creative input.
105
+ func WithSubmitRunSource(body map[string]any, source string) map[string]any {
106
+ if source = strings.TrimSpace(source); source != "" {
107
+ body["platform"] = source
108
+ }
109
+ return body
110
+ }
@@ -13,6 +13,7 @@ const (
13
13
  DefaultAuthStoreServiceName = "pippit-cli"
14
14
  SubmitRunPath = "/api/biz/v1/skill/submit_run"
15
15
  GetCreditBalancePath = "/api/biz/v1/skill/get_credit_balance"
16
+ GetAvailableModelListPath = "/api/biz/v1/skill/get_available_model_list"
16
17
  GetThreadPath = "/api/biz/v1/skill/get_thread"
17
18
  UploadFilePath = "/api/biz/v1/skill/upload_file"
18
19
  ListThreadFilePath = "/api/biz/v1/skill/list_thread_file"
@@ -30,11 +31,12 @@ type Config struct {
30
31
  }
31
32
 
32
33
  type Paths struct {
33
- SubmitRun string
34
- GetCreditBalance string
35
- GetThread string
36
- UploadFile string
37
- ListThreadFile string
34
+ SubmitRun string
35
+ GetCreditBalance string
36
+ GetAvailableModelList string
37
+ GetThread string
38
+ UploadFile string
39
+ ListThreadFile string
38
40
  }
39
41
 
40
42
  // Load resolves the built-in runtime config.
@@ -45,11 +47,12 @@ func Load() *Config {
45
47
  AuthTTL: DefaultAuthTTL,
46
48
  AccessKey: strings.TrimSpace(os.Getenv(EnvXYQAccessKey)),
47
49
  Paths: &Paths{
48
- SubmitRun: SubmitRunPath,
49
- GetCreditBalance: GetCreditBalancePath,
50
- GetThread: GetThreadPath,
51
- UploadFile: UploadFilePath,
52
- ListThreadFile: ListThreadFilePath,
50
+ SubmitRun: SubmitRunPath,
51
+ GetCreditBalance: GetCreditBalancePath,
52
+ GetAvailableModelList: GetAvailableModelListPath,
53
+ GetThread: GetThreadPath,
54
+ UploadFile: UploadFilePath,
55
+ ListThreadFile: ListThreadFilePath,
53
56
  },
54
57
  }
55
58
  }
@@ -23,6 +23,9 @@ func TestLoadUsesDefaultConfig(t *testing.T) {
23
23
  if cfg.Paths.GetCreditBalance != GetCreditBalancePath {
24
24
  t.Fatalf("GetCreditBalance path = %q, want %q", cfg.Paths.GetCreditBalance, GetCreditBalancePath)
25
25
  }
26
+ if cfg.Paths.GetAvailableModelList != GetAvailableModelListPath {
27
+ t.Fatalf("GetAvailableModelList path = %q, want %q", cfg.Paths.GetAvailableModelList, GetAvailableModelListPath)
28
+ }
26
29
  }
27
30
 
28
31
  func TestLoadReadsAccessKey(t *testing.T) {
@@ -23,6 +23,7 @@ const ratioUsage = "enum values: 0=原始比例/自动, 2=16:9(横屏), 13=21:9(
23
23
 
24
24
  // Options is the stable command-facing request shape for generate-image.
25
25
  type Options struct {
26
+ Source string
26
27
  Prompt string
27
28
  ImagePaths []string
28
29
  Model string
@@ -151,7 +152,7 @@ func buildSubmitRunBody(opts *Options, imageAssetIDs []string) map[string]any {
151
152
  if len(imageAssetIDs) > 0 {
152
153
  body["asset_ids"] = imageAssetIDs
153
154
  }
154
- return body
155
+ return common.WithSubmitRunSource(body, opts.Source)
155
156
  }
156
157
 
157
158
  func parseRatio(raw string) (*int, error) {