@pippit-dev/cli 0.0.5 → 0.0.7

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
@@ -13,7 +13,7 @@ export XYQ_ACCESS_KEY="<access-key>"
13
13
  pippit-cli short-drama +submit-run --message "写一个赛博朋克短剧开头"
14
14
  pippit-cli short-drama +upload-file --path ./story.md
15
15
  pippit-cli short-drama +get-thread --thread-id thread_123 --run-id run_456 --after-seq 0
16
- pippit-cli short-drama +download-result --output-dir ./thread_123/results/result.mp4 --url URL
16
+ pippit-cli short-drama +download-result --output-path ./thread_123/results/result.mp4 --url URL
17
17
  ```
18
18
 
19
19
  NPM package names must be lowercase, so the publishable package name is
@@ -26,7 +26,7 @@ export XYQ_ACCESS_KEY="<access-key>"
26
26
  go run . short-drama +submit-run --message "写一个赛博朋克短剧开头"
27
27
  go run . short-drama +upload-file --path ./story.md
28
28
  go run . short-drama +get-thread --thread-id thread_123 --run-id run_456 --after-seq 0
29
- go run . short-drama +download-result --output-dir ./thread_123/results/result.mp4 --url URL
29
+ go run . short-drama +download-result --output-path ./thread_123/results/result.mp4 --url URL
30
30
  ```
31
31
 
32
32
  `+submit-run` calls `/api/biz/v1/skill/submit_run` and prints `thread_id`,
@@ -34,7 +34,7 @@ go run . short-drama +download-result --output-dir ./thread_123/results/result.m
34
34
  `+get-thread` calls
35
35
  `/api/biz/v1/skill/get_thread` and prints extracted `messages`.
36
36
  `+download-result` downloads the result URL to
37
- the `--output-dir` file path. `+upload-file` is still mocked while
37
+ the `--output-path` file path. `+upload-file` is still mocked while
38
38
  its real service contract is wired.
39
39
 
40
40
  ## HTTP Client
package/checksums.txt CHANGED
@@ -1,6 +1,6 @@
1
- 8afee21204eb10b435b6ae93762af02e7a7d8c2fb2e4a7f0b613dc7b56d23d99 pippit-cli-0.0.5-darwin-amd64.tar.gz
2
- dfe02e27e30eaffe5ef7be9f3b914684a87ca72b809e635ff8c3c9f599f0aaa7 pippit-cli-0.0.5-darwin-arm64.tar.gz
3
- 0b73c06d98bf696cc3bae73381c50185c0c63a9db707a4f81adba4088452cbef pippit-cli-0.0.5-linux-amd64.tar.gz
4
- de2f89ef9124b64b5cee3bfa88cb7c0f110faefeed04517922861ccd42d641b2 pippit-cli-0.0.5-linux-arm64.tar.gz
5
- ab4a79448c29be1f129abc8f82007fb25dd636e6b9f25d13f7085826f86f48ef pippit-cli-0.0.5-windows-amd64.zip
6
- 8599eb109ad4b5a1e844e5475e02fc2ddd4d91057cf94f164570ac632cbaa43a pippit-cli-0.0.5-windows-arm64.zip
1
+ b2cba39200b8114a21e496bca83d677fe2b478b23e374666a0a8cab2ad2e457e pippit-cli-0.0.7-darwin-amd64.tar.gz
2
+ 44be0c9cb8dc98edec7519a704290c06dcfa966360c48bcad709f1e53d5a80d0 pippit-cli-0.0.7-darwin-arm64.tar.gz
3
+ 175816681f3affd9eb20900c6e0cfbc035ec1f24eaf06dc6c41686b6cdea5ec7 pippit-cli-0.0.7-linux-amd64.tar.gz
4
+ ebf0c0e9b4b2bbed872acce3f9cf07dd0b21c52ab7c11c62d6778f820c0479fa pippit-cli-0.0.7-linux-arm64.tar.gz
5
+ 573c22cdbd208e9a9a541ae4f67cd922e63129995716d7b92e665d828cec8f49 pippit-cli-0.0.7-windows-amd64.zip
6
+ 996d18e5ca27083ee8e9b200f39571c53ff8829f7fdc85c8ba7631778e7f3178 pippit-cli-0.0.7-windows-arm64.zip
@@ -96,9 +96,9 @@ func newShortDramaDownloadResultCommand(stdout, stderr io.Writer, runner *common
96
96
  Short: "Download a generated result URL",
97
97
  Args: cobra.NoArgs,
98
98
  RunE: func(cmd *cobra.Command, _ []string) error {
99
- opts.OutputDir = strings.TrimSpace(opts.OutputDir)
100
- if opts.OutputDir == "" {
101
- return fmt.Errorf("--output-dir is required")
99
+ opts.OutputPath = strings.TrimSpace(opts.OutputPath)
100
+ if opts.OutputPath == "" {
101
+ return fmt.Errorf("--output-path is required")
102
102
  }
103
103
  opts.URL = strings.TrimSpace(opts.URL)
104
104
  if opts.URL == "" {
@@ -118,7 +118,7 @@ func newShortDramaDownloadResultCommand(stdout, stderr io.Writer, runner *common
118
118
  cmd.SetOut(stdout)
119
119
  cmd.SetErr(stderr)
120
120
  cmd.Flags().StringVar(&opts.URL, "url", "", "URL to download")
121
- cmd.Flags().StringVar(&opts.OutputDir, "output-dir", "", "output file path")
121
+ cmd.Flags().StringVar(&opts.OutputPath, "output-path", "", "local output file path")
122
122
  cmd.Flags().IntVar(&opts.Workers, "workers", 5, "parallel download workers")
123
123
  return cmd
124
124
  }
@@ -164,8 +164,8 @@ func newShortDramaListThreadFileCommand(stdout, stderr io.Writer, runner *common
164
164
  if opts.ThreadID == "" {
165
165
  return fmt.Errorf("--thread-id is required")
166
166
  }
167
- if opts.PageSize <= 0 || opts.PageSize > 100 {
168
- return fmt.Errorf("--page-size must be between 1 and 100")
167
+ if opts.PageSize <= 0 || opts.PageSize > 1000 {
168
+ return fmt.Errorf("--page-size must be between 1 and 1000")
169
169
  }
170
170
  if opts.PageNum <= 0 {
171
171
  return fmt.Errorf("--page-num must be greater than 0")
@@ -182,7 +182,7 @@ func newShortDramaListThreadFileCommand(stdout, stderr io.Writer, runner *common
182
182
  cmd.SetErr(stderr)
183
183
  cmd.Flags().StringVar(&opts.ThreadID, "thread-id", "", "thread ID to list files for")
184
184
  cmd.Flags().IntVar(&opts.PageNum, "page-num", 1, "page number (1-based)")
185
- cmd.Flags().IntVar(&opts.PageSize, "page-size", 100, "number of files per page (between 1 and 100)")
185
+ cmd.Flags().IntVar(&opts.PageSize, "page-size", 1000, "number of files per page (between 1 and 1000)")
186
186
  return cmd
187
187
  }
188
188
 
@@ -181,7 +181,7 @@ func TestShortDramaDownloadResult(t *testing.T) {
181
181
  outputPath := filepath.Join("results", "cover.jpeg")
182
182
  root.SetArgs([]string{
183
183
  "short-drama", "+download-result",
184
- "--output-dir", outputPath,
184
+ "--output-path", outputPath,
185
185
  "--workers", "2",
186
186
  "--url", server.URL + "/image?filename=ignored.jpeg",
187
187
  })
@@ -191,11 +191,11 @@ func TestShortDramaDownloadResult(t *testing.T) {
191
191
  }
192
192
 
193
193
  got := decodeJSON(t, stdout.Bytes())
194
- if got["output_dir"] != outputPath {
195
- t.Fatalf("output_dir = %v, want %s", got["output_dir"], outputPath)
194
+ if got["output_path"] != outputPath {
195
+ t.Fatalf("output_path = %v, want %s", got["output_path"], outputPath)
196
196
  }
197
- if got["total"] != float64(1) {
198
- t.Fatalf("total = %v, want 1", got["total"])
197
+ if _, ok := got["total"]; ok {
198
+ t.Fatalf("total should not be returned: %#v", got)
199
199
  }
200
200
  downloaded, ok := got["downloaded"].([]any)
201
201
  if !ok || len(downloaded) != 1 {
@@ -233,7 +233,7 @@ func TestShortDramaDownloadResultSkipsExistingFile(t *testing.T) {
233
233
  root := NewRootCommand(&stdout, &stderr)
234
234
  root.SetArgs([]string{
235
235
  "short-drama", "+download-result",
236
- "--output-dir", outputPath,
236
+ "--output-path", outputPath,
237
237
  "--url", server.URL + "/image",
238
238
  })
239
239
 
@@ -245,11 +245,11 @@ func TestShortDramaDownloadResultSkipsExistingFile(t *testing.T) {
245
245
  }
246
246
 
247
247
  got := decodeJSON(t, stdout.Bytes())
248
- if got["output_dir"] != outputPath {
249
- t.Fatalf("output_dir = %v, want %s", got["output_dir"], outputPath)
248
+ if got["output_path"] != outputPath {
249
+ t.Fatalf("output_path = %v, want %s", got["output_path"], outputPath)
250
250
  }
251
- if got["total"] != float64(0) {
252
- t.Fatalf("total = %v, want 0", got["total"])
251
+ if _, ok := got["total"]; ok {
252
+ t.Fatalf("total should not be returned: %#v", got)
253
253
  }
254
254
  alreadyExist, ok := got["already_exist"].([]any)
255
255
  if !ok || len(alreadyExist) != 1 || alreadyExist[0] != outputPath {
@@ -258,7 +258,49 @@ func TestShortDramaDownloadResultSkipsExistingFile(t *testing.T) {
258
258
  assertFileContent(t, outputPath, "existing-data")
259
259
  }
260
260
 
261
- func TestShortDramaDownloadResultRequiresOutputDir(t *testing.T) {
261
+ func TestShortDramaDownloadResultSkipsMetaJSON(t *testing.T) {
262
+ serverCalled := false
263
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
264
+ serverCalled = true
265
+ t.Fatal("server should not receive request for meta.json")
266
+ }))
267
+ defer server.Close()
268
+
269
+ chdirTemp(t)
270
+ outputPath := filepath.Join("results", "meta.json")
271
+
272
+ var stdout, stderr bytes.Buffer
273
+ root := NewRootCommand(&stdout, &stderr)
274
+ root.SetArgs([]string{
275
+ "short-drama", "+download-result",
276
+ "--output-path", outputPath,
277
+ "--url", server.URL + "/meta.json",
278
+ })
279
+
280
+ if err := root.Execute(); err != nil {
281
+ t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
282
+ }
283
+ if serverCalled {
284
+ t.Fatal("server was called, want meta.json to skip download")
285
+ }
286
+ if _, err := os.Stat(outputPath); !os.IsNotExist(err) {
287
+ t.Fatalf("meta.json stat err = %v, want file to be absent", err)
288
+ }
289
+
290
+ got := decodeJSON(t, stdout.Bytes())
291
+ if got["output_path"] != outputPath {
292
+ t.Fatalf("output_path = %v, want %s", got["output_path"], outputPath)
293
+ }
294
+ if _, ok := got["total"]; ok {
295
+ t.Fatalf("total should not be returned: %#v", got)
296
+ }
297
+ skipped, ok := got["skipped"].([]any)
298
+ if !ok || len(skipped) != 1 || skipped[0] != outputPath {
299
+ t.Fatalf("skipped = %#v, want meta.json output path", got["skipped"])
300
+ }
301
+ }
302
+
303
+ func TestShortDramaDownloadResultRequiresOutputPath(t *testing.T) {
262
304
  var stdout, stderr bytes.Buffer
263
305
  root := NewRootCommand(&stdout, &stderr)
264
306
  root.SetArgs([]string{"short-drama", "+download-result", "--url", "https://example.com/image.png"})
@@ -267,15 +309,33 @@ func TestShortDramaDownloadResultRequiresOutputDir(t *testing.T) {
267
309
  if err == nil {
268
310
  t.Fatal("Execute() error = nil, want validation error")
269
311
  }
270
- if !strings.Contains(err.Error(), "--output-dir is required") {
271
- t.Fatalf("error = %q, want output-dir validation", err)
312
+ if !strings.Contains(err.Error(), "--output-path is required") {
313
+ t.Fatalf("error = %q, want output-path validation", err)
314
+ }
315
+ }
316
+
317
+ func TestShortDramaDownloadResultRejectsOutputDirFlag(t *testing.T) {
318
+ var stdout, stderr bytes.Buffer
319
+ root := NewRootCommand(&stdout, &stderr)
320
+ root.SetArgs([]string{
321
+ "short-drama", "+download-result",
322
+ "--output-dir", filepath.Join("results", "image.png"),
323
+ "--url", "https://example.com/image.png",
324
+ })
325
+
326
+ err := root.Execute()
327
+ if err == nil {
328
+ t.Fatal("Execute() error = nil, want unknown flag error")
329
+ }
330
+ if !strings.Contains(err.Error(), "unknown flag: --output-dir") {
331
+ t.Fatalf("error = %q, want output-dir rejection", err)
272
332
  }
273
333
  }
274
334
 
275
335
  func TestShortDramaDownloadResultRequiresURL(t *testing.T) {
276
336
  var stdout, stderr bytes.Buffer
277
337
  root := NewRootCommand(&stdout, &stderr)
278
- root.SetArgs([]string{"short-drama", "+download-result", "--output-dir", filepath.Join("results", "image.png")})
338
+ root.SetArgs([]string{"short-drama", "+download-result", "--output-path", filepath.Join("results", "image.png")})
279
339
 
280
340
  err := root.Execute()
281
341
  if err == nil {
@@ -289,7 +349,7 @@ func TestShortDramaDownloadResultRequiresURL(t *testing.T) {
289
349
  func TestShortDramaDownloadResultRejectsInvalidScheme(t *testing.T) {
290
350
  var stdout, stderr bytes.Buffer
291
351
  root := NewRootCommand(&stdout, &stderr)
292
- root.SetArgs([]string{"short-drama", "+download-result", "--output-dir", filepath.Join("results", "image.png"), "--url", "file:///etc/passwd"})
352
+ root.SetArgs([]string{"short-drama", "+download-result", "--output-path", filepath.Join("results", "image.png"), "--url", "file:///etc/passwd"})
293
353
 
294
354
  err := root.Execute()
295
355
  if err == nil {
@@ -311,7 +371,7 @@ func TestShortDramaDownloadResultAllFailed(t *testing.T) {
311
371
  root := NewRootCommand(&stdout, &stderr)
312
372
  root.SetArgs([]string{
313
373
  "short-drama", "+download-result",
314
- "--output-dir", filepath.Join("results", "missing.png"),
374
+ "--output-path", filepath.Join("results", "missing.png"),
315
375
  "--url", server.URL + "/notfound",
316
376
  })
317
377
 
@@ -337,7 +397,7 @@ func TestShortDramaDownloadResultOutputPath(t *testing.T) {
337
397
  outputPath := filepath.Join("custom", "nested", "cover.png")
338
398
  root.SetArgs([]string{
339
399
  "short-drama", "+download-result",
340
- "--output-dir", outputPath,
400
+ "--output-path", outputPath,
341
401
  "--url", server.URL + "/image.png",
342
402
  })
343
403
 
@@ -346,8 +406,8 @@ func TestShortDramaDownloadResultOutputPath(t *testing.T) {
346
406
  }
347
407
 
348
408
  got := decodeJSON(t, stdout.Bytes())
349
- if got["output_dir"] != outputPath {
350
- t.Fatalf("output_dir = %v, want %s", got["output_dir"], outputPath)
409
+ if got["output_path"] != outputPath {
410
+ t.Fatalf("output_path = %v, want %s", got["output_path"], outputPath)
351
411
  }
352
412
  assertFileContent(t, outputPath, "image-data")
353
413
  }
@@ -497,6 +557,10 @@ func TestShortDramaListThreadFile(t *testing.T) {
497
557
  if got["total"] != float64(1) {
498
558
  t.Fatalf("total = %v, want 1", got["total"])
499
559
  }
560
+ wantMessage := "<system-remind>\n- total is below 1000; continue querying with current --page-num 2\n</system-remind>"
561
+ if got["message"] != wantMessage {
562
+ t.Fatalf("message = %v, want current page hint", got["message"])
563
+ }
500
564
  files, ok := got["files"].([]any)
501
565
  if !ok || len(files) != 1 {
502
566
  t.Fatalf("files = %#v, want one file", got["files"])
@@ -517,6 +581,35 @@ func TestShortDramaListThreadFile(t *testing.T) {
517
581
  }
518
582
  }
519
583
 
584
+ func TestShortDramaListThreadFileMessageWhenPageFull(t *testing.T) {
585
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
586
+ if !strings.Contains(r.URL.Path, "list_thread_file") {
587
+ t.Fatalf("path = %s, want list_thread_file path", r.URL.Path)
588
+ }
589
+ _, _ = w.Write([]byte(`{"ret":"0","errmsg":"","data":{"total":1000,"files":[]}}`))
590
+ }))
591
+ defer server.Close()
592
+
593
+ var stdout, stderr bytes.Buffer
594
+ root := newTestRootCommand(t, &stdout, &stderr, server.URL)
595
+ root.SetArgs([]string{
596
+ "short-drama", "+list-thread-file",
597
+ "--thread-id", "thread_123",
598
+ "--page-num", "2",
599
+ "--page-size", "1000",
600
+ })
601
+
602
+ if err := root.Execute(); err != nil {
603
+ t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
604
+ }
605
+
606
+ got := decodeJSON(t, stdout.Bytes())
607
+ wantMessage := "<system-remind>\n- total reached 1000; query the next page with --page-num 3\n</system-remind>"
608
+ if got["message"] != wantMessage {
609
+ t.Fatalf("message = %v, want next page hint", got["message"])
610
+ }
611
+ }
612
+
520
613
  func TestShortDramaListThreadFileRequiresThreadID(t *testing.T) {
521
614
  var stdout, stderr bytes.Buffer
522
615
  root := NewRootCommand(&stdout, &stderr)
@@ -21,9 +21,9 @@ import (
21
21
 
22
22
  // DownloadResultOptions is the command-facing shape for downloading one result URL.
23
23
  type DownloadResultOptions struct {
24
- URL string `json:"url"`
25
- OutputDir string `json:"output_dir"`
26
- Workers int `json:"workers"`
24
+ URL string `json:"url"`
25
+ OutputPath string `json:"output_path"`
26
+ Workers int `json:"workers"`
27
27
  }
28
28
 
29
29
  type DownloadResultError struct {
@@ -33,10 +33,10 @@ type DownloadResultError struct {
33
33
 
34
34
  // DownloadResultResponse is the JSON envelope printed by `pippit-cli short-drama +download-result`.
35
35
  type DownloadResultResponse struct {
36
- OutputDir string `json:"output_dir"`
36
+ OutputPath string `json:"output_path"`
37
37
  Downloaded []string `json:"downloaded"`
38
38
  AlreadyExist []string `json:"already_exist,omitempty"`
39
- Total int `json:"total"`
39
+ Skipped []string `json:"skipped,omitempty"`
40
40
  Errors []*DownloadResultError `json:"errors,omitempty"`
41
41
  }
42
42
 
@@ -64,9 +64,15 @@ func DownloadResult(ctx context.Context, opts DownloadResultOptions, runner *Run
64
64
  return nil, fmt.Errorf("download url is required")
65
65
  }
66
66
 
67
- outputPath := strings.TrimSpace(opts.OutputDir)
67
+ outputPath := strings.TrimSpace(opts.OutputPath)
68
68
  if outputPath == "" {
69
- return nil, fmt.Errorf("output_dir is required")
69
+ return nil, fmt.Errorf("output_path is required")
70
+ }
71
+ if shouldSkipDownload(outputPath) {
72
+ return &DownloadResultResponse{
73
+ OutputPath: outputPath,
74
+ Skipped: []string{outputPath},
75
+ }, nil
70
76
  }
71
77
  // check if the output path exists and is a file
72
78
  if info, err := os.Stat(outputPath); err == nil {
@@ -74,9 +80,8 @@ func DownloadResult(ctx context.Context, opts DownloadResultOptions, runner *Run
74
80
  return nil, fmt.Errorf("output path %q is a directory", outputPath)
75
81
  }
76
82
  return &DownloadResultResponse{
77
- OutputDir: outputPath,
83
+ OutputPath: outputPath,
78
84
  AlreadyExist: []string{outputPath},
79
- Total: 0,
80
85
  }, nil
81
86
  } else if !os.IsNotExist(err) {
82
87
  return nil, fmt.Errorf("stat output path: %w", err)
@@ -159,9 +164,8 @@ func DownloadResult(ctx context.Context, opts DownloadResultOptions, runner *Run
159
164
  })
160
165
 
161
166
  result := &DownloadResultResponse{
162
- OutputDir: outputPath,
167
+ OutputPath: outputPath,
163
168
  Downloaded: downloaded,
164
- Total: len(downloaded),
165
169
  Errors: errorList,
166
170
  }
167
171
  if len(downloaded) == 0 && len(errorList) > 0 {
@@ -170,6 +174,10 @@ func DownloadResult(ctx context.Context, opts DownloadResultOptions, runner *Run
170
174
  return result, nil
171
175
  }
172
176
 
177
+ func shouldSkipDownload(outputPath string) bool {
178
+ return filepath.Base(outputPath) == "meta.json"
179
+ }
180
+
173
181
  func downloadFileWithRetry(ctx context.Context, client Client, rawURL string, targetPath string) error {
174
182
  var lastErr error
175
183
  for attempt := 0; attempt <= maxDownloadRetries; attempt++ {
@@ -24,8 +24,9 @@ type ThreadFile struct {
24
24
 
25
25
  // ListThreadFileResult is the JSON envelope printed by `pippit-cli short-drama +list-thread-file`.
26
26
  type ListThreadFileResult struct {
27
- Files []*ThreadFile `json:"files"`
28
- Total int64 `json:"total"`
27
+ Files []*ThreadFile `json:"files"`
28
+ Total int64 `json:"total"`
29
+ Message string `json:"message"`
29
30
  }
30
31
 
31
32
  type listThreadFileResponse struct {
@@ -82,11 +83,24 @@ func ListThreadFile(ctx context.Context, opts *ListThreadFileOptions, runner *Ru
82
83
  }
83
84
 
84
85
  return &ListThreadFileResult{
85
- Files: files,
86
- Total: resp.Data.Total,
86
+ Files: files,
87
+ Total: resp.Data.Total,
88
+ Message: listThreadFileMessage(resp.Data.Total, opts.PageNum),
87
89
  }, nil
88
90
  }
89
91
 
92
+ func listThreadFileMessage(total int64, pageNum int) string {
93
+ start, end := "<system-remind>", "</system-remind>"
94
+ if pageNum <= 0 {
95
+ pageNum = 1
96
+ }
97
+ var message string
98
+ if total >= 1000 {
99
+ message = fmt.Sprintf("total reached 1000; query the next page with --page-num %d", pageNum+1)
100
+ }
101
+ return fmt.Sprintf("%s\n- %s\n%s", start, message, end)
102
+ }
103
+
90
104
  func threadFilePath(threadID string, filePath string) string {
91
105
  parts := []string{strings.TrimSpace(threadID)}
92
106
  trimmedFilePath := strings.Trim(strings.TrimSpace(filePath), `/\`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pippit-dev/cli",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Pippit CLI",
5
5
  "bin": {
6
6
  "pippit-cli": "scripts/run.js"
@@ -17,17 +17,19 @@ metadata:
17
17
 
18
18
  # 小云雀短剧创作
19
19
 
20
- 通过 `pippit-cli short-drama` 命令提交短剧创作任务、上传参考文件,并查询任务进展,获取会话产物文件。
20
+ 通过 `pippit-cli short-drama` 命令提交短剧创作任务、上传参考文件,并行查询任务进展和会话产物文件,及时把重要资产下载到用户本地。
21
21
 
22
- 短剧场景面向剧情、人物、分集与画面化叙事创作,用户的原始需求通过 `--message` 发送给后端 Agent。后端 Agent 负责理解任务、编排流程和生成内容;用户侧 Agent 只负责提交任务、查询进展和展示结果。
22
+ 短剧场景面向剧情、人物、分集与画面化叙事创作,用户的原始需求通过 `--message` 发送给后端 Agent。后端 Agent 负责理解任务、编排流程和生成内容;用户侧 Agent 负责提交任务、并行查询进展与产物、主动下载重要资产并展示结果。
23
23
 
24
24
  ## 功能
25
25
 
26
26
  1. **提交短剧 Run 任务** - 创建新会话或向已有会话发送短剧创作需求。
27
27
  2. **查询会话进展** - 根据 `thread_id`、`run_id`、`after_seq` 拉取短剧任务消息列表。
28
28
  3. **上传文件** - 上传短剧相关参考文件,得到文件 ID,供后续任务引用。
29
- 4. **获取会话文件** - 根据 `thread_id` 拉取会话文件列表,得到 `file_path`、`file_name`、`download_url`。
30
- 5. **下载文件资源** - 使用文件列表中的 `download_url` 下载资源,并按 `file_path` 写入用户本地目录。
29
+ 4. **获取会话文件** - 根据 `thread_id` 拉取会话文件列表,得到 `file_path`、`download_url`。这和查询会话进展同等重要。
30
+ 5. **下载重要资产** - 使用文件列表中的 `download_url` 下载资源,并按 `file_path` 写入用户本地目标文件路径。
31
+
32
+ 重要资产包括但不限于:剧本设计、场景设计、场景图、人物角色设计、人物图、分集草稿、故事板、最终视频产物。只要 `+list-thread-file` 返回了这些资产的 `download_url`,就要及时调用下载工具落盘,不要只展示文件元信息。
31
33
 
32
34
  ## 前置要求
33
35
 
@@ -73,7 +75,7 @@ pippit-cli short-drama +upload-file --path /path/to/outline.md
73
75
 
74
76
  ```bash
75
77
  # 获取会话文件列表
76
- pippit-cli short-drama +list-thread-file --thread-id THREAD_ID --page-num 1 --page-size 100
78
+ pippit-cli short-drama +list-thread-file --thread-id THREAD_ID --page-num 1 --page-size 1000
77
79
  ```
78
80
 
79
81
  `+list-thread-file` 返回的每个文件对象包含:
@@ -85,16 +87,16 @@ pippit-cli short-drama +list-thread-file --thread-id THREAD_ID --page-num 1 --pa
85
87
  }
86
88
  ```
87
89
 
88
- `+list-thread-file` 只负责获取会话文件列表,不负责下载文件。
90
+ `+list-thread-file` 只负责获取会话文件列表,不负责下载文件,也不需要判断本地文件是否已存在。
89
91
 
90
92
  ### 5. 下载文件资源
91
93
 
92
94
  ```bash
93
- # 下载文件资源到指定目录
94
- pippit-cli short-drama +download-result --url DOWNLOAD_URL --output-dir FILE_PATH
95
+ # 下载文件资源到指定文件路径
96
+ pippit-cli short-drama +download-result --url DOWNLOAD_URL --output-path FILE_PATH
95
97
  ```
96
98
 
97
- `+download-result` 负责把会话产生的文件,通过URL下载到 `file_path`目录中,如果目标文件已存在,跳过下载。
99
+ `FILE_PATH` 必须直接使用 `+list-thread-file` 返回的完整 `file_path`,包含文件名,不要取父目录。`+download-result` 负责把会话产生的文件通过 URL 下载到该目标文件路径;如果目标文件已存在,跳过下载。
98
100
 
99
101
  ## 典型工作流
100
102
 
@@ -104,20 +106,25 @@ pippit-cli short-drama +download-result --url DOWNLOAD_URL --output-dir FILE_PAT
104
106
  1. pippit-cli short-drama +submit-run --message "用户的原始短剧需求"
105
107
  → 拿到 thread_id、run_id 和 web_thread_link
106
108
  2. 立即将 web_thread_link 展示给用户
107
- 3. 并行发起:
109
+ 3. 并行发起,二者同等重要:
108
110
  a. pippit-cli short-drama +get-thread --thread-id THREAD_ID --run-id RUN_ID --after-seq SEQUENCE
109
- b. pippit-cli short-drama +list-thread-file --thread-id THREAD_ID --page-num 1 --page-size 100
111
+ b. pippit-cli short-drama +list-thread-file --thread-id THREAD_ID --page-num PAGE_NUM --page-size 1000
110
112
  4. 检查 `get-thread` 返回的 messages:
111
113
  - 如果任务仍在进行中:展示过程消息,继续查询
112
114
  - 如果后端 Agent 提出问题:展示问题,等待用户回复
113
- 5. 解析 `list-thread-file` 返回的 files,只获取文件元信息:
114
- - 对每个文件取 file_path、file_name、download_url
115
- - file_path 在用户本地环境构建目录
116
- - 如果在工作目录中 file_name 已存在:跳过下载
117
- 6. 对缺失的本地文件,调用 +download-result 并行下载资源:
115
+ 5. 检查 `list-thread-file` 返回的 files
116
+ - 对每个文件取 file_path、download_url
117
+ - file_path 作为本地目标文件路径,包含文件名
118
+ - download_url 的重要资产:加入本轮下载队列
119
+ - 不判断 file_path 在本地是否已存在,是否跳过由 +download-result 内部处理
120
+ - 如果本轮 total 达到 1000:下一轮将 PAGE_NUM 加 1,继续查询新一页文件
121
+ 6. 对重要资产,立即调用 +download-result 并行下载资源:
118
122
  - 使用第 5 步获取的 download_url 作为 --url
119
- - 使用第 5 步获取的 file_path 作为 --output-dir
120
- 7. 如用户继续追加需求,使用同一 thread_id 再次 submit-run
123
+ - 使用第 5 步获取的完整 file_path 作为 --output-path
124
+ - 剧本设计、场景设计、场景图、人物角色设计、人物图、最终视频产物都属于重要资产
125
+ 7. 查询或下载失败时,不要直接放弃;记录失败项,并在后续轮询中主动重试
126
+ 8. 只有会话进展已处理,且已发现的重要资产均已下载或明确重试失败后,才向用户汇总最终结果
127
+ 9. 如用户继续追加需求,使用同一 thread_id 再次 submit-run
121
128
  ```
122
129
 
123
130
  ### 场景 2:用户提供参考文件要求创作
@@ -127,7 +134,7 @@ pippit-cli short-drama +download-result --url DOWNLOAD_URL --output-dir FILE_PAT
127
134
  → 拿到 file_id
128
135
  2. pippit-cli short-drama +submit-run --message "用户的原始短剧需求" --asset-ids file_id
129
136
  → 拿到 thread_id、run_id 和 web_thread_link
130
- 3. 后续同场景 1 的并行查询和文件下载流程
137
+ 3. 后续同场景 1 的并行查询、重要资产发现和文件下载流程
131
138
  ```
132
139
 
133
140
  ### 场景 3:在已有短剧会话中续写或修改
@@ -135,18 +142,30 @@ pippit-cli short-drama +download-result --url DOWNLOAD_URL --output-dir FILE_PAT
135
142
  ```
136
143
  1. pippit-cli short-drama +submit-run --message "用户的新需求" --thread-id THREAD_ID
137
144
  → 拿到新的 run_id 和 web_thread_link
138
- 2. 继续按场景 1 展示进展、处理用户补充问题、获取新增会话文件列表,并按需下载新增文件资源
145
+ 2. 继续按场景 1 展示进展、处理用户补充问题、获取新增会话文件列表,并及时下载新增重要资产
139
146
  ```
140
147
 
141
148
  ## 轮询策略
142
149
 
143
150
  - **间隔**:每 10 秒查询一次。
144
151
  - **增量拉取**:首次使用 `--after-seq 0`,后续根据已读消息进度调整 `after-seq`。
145
- - **并行查询**:每次 `+submit-run` 返回 `thread_id` 后,同时发起 `+get-thread` 和 `+list-thread-file`;会话信息展示流程保持不变,`+list-thread-file` 只用于获取文件元信息。
146
- - **文件下载**:解析 `+list-thread-file` 的结果后,只有目标本地文件不存在时才调用 `+download-result` 下载资源。
152
+ - **并行查询**:每次 `+submit-run` 返回 `thread_id` 后,同时发起 `+get-thread` 和 `+list-thread-file`;二者同等重要,不能只查询会话进展而忽略会话文件。
153
+ - **文件分页**:`+list-thread-file` 使用 `--page-size 1000`。如果本轮返回的 `total` 达到 1000,下一轮使用 `--page-num` 加 1 查询新一页结果;如果未达到 1000,保持当前页继续轮询新增产物。
154
+ - **重要资产识别**:每轮都检查 `+list-thread-file` 返回的文件。剧本设计、场景设计、场景图、人物角色设计、人物图、分集草稿、故事板、最终视频产物都是重要资产。
155
+ - **文件下载**:解析 `+list-thread-file` 的结果后,对带 `download_url` 的重要资产立即调用 `+download-result` 下载资源;不要在 `list-thread-file` 阶段检查文件是否已存在,存在性检查由下载工具内部处理。
156
+ - **下载完成标准**:不要把文件元信息展示当成下载完成;必须拿到本地 `file_path`,或明确记录该文件在重试后仍下载失败。
147
157
  - **用户确认**:如果消息中出现需要用户确认、补充设定或回答问题的内容,先展示给用户,等待用户回复。
148
158
  - **超时**:如果长时间无结果,告知用户任务仍在生成中,可稍后通过 `web_thread_link` 查看。
149
- - **错误处理**:单次查询失败可重试;连续失败时停止轮询并向用户说明错误。
159
+ - **错误处理**:`+get-thread`、`+list-thread-file` 或 `+download-result` 任一调用失败时,记录失败原因和参数,在后续轮询中主动重试;重试期间继续处理其他成功返回的消息和文件。连续多轮失败后再向用户说明仍未完成的查询或下载项。
160
+
161
+ ## 完成标准
162
+
163
+ 一次短剧任务不能只以 `+get-thread` 返回完成消息作为结束条件。完成前必须同时检查:
164
+
165
+ 1. 已处理 `+get-thread` 返回的最新会话进展、用户确认问题和最终消息。
166
+ 2. 已用 `--page-size 1000` 调用 `+list-thread-file` 获取会话文件列表;如果本轮 `total` 达到 1000,已在后续轮询中递增 `page-num` 查询新一页。
167
+ 3. 对所有带 `download_url` 的重要资产,已调用 `+download-result` 下载到本地 `file_path`。
168
+ 4. 对查询失败或下载失败的资产,已在后续轮询中主动重试,并在最终回复中列出仍失败的文件或命令。
150
169
 
151
170
  ## 输出格式
152
171
 
@@ -204,51 +223,59 @@ pippit-cli short-drama +download-result --url DOWNLOAD_URL --output-dir FILE_PAT
204
223
  "download_url": "https://..."
205
224
  }
206
225
  ],
207
- "total": 1
226
+ "total": 1,
227
+ "message": "<system-remind>\n- total reached 1000; query the next page with --page-num {page-num} + 1\n</system-remind>"
208
228
  }
209
229
  ```
210
230
 
231
+ 当 `total` 达到 1000 时,`message` 会用 `<system-remind>` 提示下一轮将 `page-num` 加 1 查询新一页。
232
+
211
233
  **+download-result** 返回:
212
234
 
213
235
  ```json
214
236
  {
215
- "output_dir": "/path/to/output-dir",
216
- "downloaded": ["/path/to/output-dir/01.md"],
217
- "total": 1
237
+ "output_path": "./{thread-id}/{file_path}/{file_name}",
238
+ "downloaded": ["./{thread-id}/{file_path}/{file_name}"]
218
239
  }
219
240
  ```
220
241
 
221
242
  ## 会话文件与资源下载
222
243
 
223
- 先用 `+list-thread-file` 获取会话文件列表,再按需用 `+download-result` 并行下载文件资源。
244
+ 先用 `+list-thread-file` 获取会话文件列表,再用 `+download-result` 并行下载重要资产。获取文件元信息不是最终目标,重要资产落盘才是核心目标。文件是否已存在由下载工具内部检查,`+list-thread-file` 阶段不要做本地存在性判断。
224
245
 
225
246
  ### 获取会话文件
226
247
 
227
- 从 `+list-thread-file` 的 `files` 中逐个读取文件元信息:`file_path`、`download_url`。
248
+ 从 `+list-thread-file` 的 `files` 中逐个读取文件元信息:`file_path`、`file_name`、`download_url`。重点识别剧本设计、场景设计、场景图、人物角色设计、人物图、分集草稿、故事板、最终视频产物等重要资产。
228
249
 
229
250
  ```
230
- 1. file_path在本地已存在
231
- → 跳过
232
- 2. file_path在本地不存在
251
+ 1. 有download_url的重要资产
233
252
  → 记录该file_path和URL
234
- 多个待下载的文件 → 使用 +download-result 工具并行下载文件到本地
253
+ → 使用 +download-result 将URL资源下载到该file_path
254
+ 2. 本轮total达到1000
255
+ → 下一轮page-num加1,继续查询新一页结果
256
+ 3. 本轮total未达到1000
257
+ → 后续轮询保持当前页,继续发现新增产物
258
+ 4. list-thread-file或download-result失败
259
+ → 记录失败参数和错误
260
+ → 后续轮询主动重试,不要直接结束任务
235
261
  ```
236
262
 
237
263
  ### 并行下载文件资源
238
264
 
239
- 只对目标路径不存在的文件调用下载工具,可并行:
265
+ 对带 `download_url` 的重要资产调用下载工具,可并行。重要资产必须主动下载,不要等用户再次要求,也不要在调用下载工具前先检查本地文件是否存在。
240
266
 
241
- 1. 调用 `pippit-cli short-drama +download-result --url DOWNLOAD_URL --output-dir FILE_PATH`。
242
- 2. 下载完成后,向用户展示本地文件路径;如果某个文件下载失败,只报告该文件错误,不阻塞已成功落盘的文件展示。
267
+ 1. 调用 `pippit-cli short-drama +download-result --url DOWNLOAD_URL --output-path FILE_PATH`。
268
+ 2. 下载完成后,向用户展示本地文件路径;如果某个文件下载失败,记录失败项并在后续轮询中重试,不阻塞已成功落盘的文件展示。
243
269
 
244
270
  ## 向用户展示内容
245
271
 
246
272
  - 任务提交后:立即展示 `web_thread_link`。
247
273
  - 任务进行中:展示后端 Agent 返回的过程消息。
248
274
  - 需要用户补充信息时:原样展示后端 Agent 的问题,等待用户回复。
249
- - 任务完成后:展示短剧内容、分集草稿、设定说明或其他结果信息。
275
+ - 任务完成后:展示短剧内容、分集草稿、设定说明或其他结果信息,同时检查是否有未下载的重要资产。
250
276
  - 获取会话文件后:展示或记录文件元信息,不把它当成已下载结果。
251
277
  - 文件资源下载后:展示已落盘的本地文件路径;已存在而跳过下载的文件也要标明。
278
+ - 如果仍有重要资产下载失败:说明失败文件、失败命令和已进行的重试,不要把它描述为已完成下载。
252
279
 
253
280
  ## 核心原则:用户侧不做创作,只做传话
254
281
 
@@ -275,6 +302,6 @@ pippit-cli short-drama +download-result --url DOWNLOAD_URL --output-dir FILE_PAT
275
302
  - 查询进展时优先使用 `+submit-run` 返回的 `thread_id` 和 `run_id`。
276
303
  - `--after-seq` 用于增量拉取消息,首次查询可设置为 `0`。
277
304
  - `+upload-file` 当前用于短剧场景文件上传链路,上传后将返回可传给 `+submit-run` 的文件 ID。
278
- - `+list-thread-file` 只需要 `thread_id`;分页参数默认 `--page-num 1 --page-size 100`。
279
- - `+list-thread-file` 和 `+download-result` 是两个不同的 CLI 指令:前者获取会话文件元信息,后者下载 URL 资源并写入到本地目录。
280
- - `+download-result` 接收 `--url`、`--output-dir`、`--workers`。
305
+ - `+list-thread-file` 只需要 `thread_id`;分页参数使用 `--page-num 1 --page-size 1000` 起步,`total` 达到 1000 时下一轮递增 `page-num`。
306
+ - `+list-thread-file` 和 `+download-result` 是两个不同的 CLI 指令:前者获取会话文件元信息,后者下载 URL 资源并写入到本地目标文件路径。
307
+ - `+download-result` 接收 `--url`、`--output-path`、`--workers`;`--output-path` 必须是包含文件名的目标文件路径。