@pippit-dev/cli 0.0.20 → 0.0.22
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 +2 -2
- package/checksums.txt +6 -6
- package/cmd/short_drama/short_drama.go +6 -1
- package/cmd/short_drama_test.go +121 -1
- package/internal/common/download_results.go +35 -16
- package/internal/common/list_thread_file.go +3 -0
- package/package.json +2 -2
- package/scripts/version-check.js +2 -1
- package/skills/short-drama/SKILL.md +13 -10
package/README.md
CHANGED
|
@@ -198,13 +198,13 @@ pippit-tool-cli --version
|
|
|
198
198
|
pippit-tool-cli short-drama +submit-run --message "写一个赛博朋克短剧开头"
|
|
199
199
|
pippit-tool-cli short-drama +upload-file --path ./reference.doc
|
|
200
200
|
pippit-tool-cli short-drama +get-thread --thread-id thread_123 --run-id run_456
|
|
201
|
-
pippit-tool-cli short-drama +download-result --output-path ./thread_123/results/result.mp4 --url URL
|
|
201
|
+
pippit-tool-cli short-drama +download-result --output-path ./thread_123/results/result.mp4 --url URL --updated-at 1779716734
|
|
202
202
|
```
|
|
203
203
|
|
|
204
204
|
`+submit-run`: 输出 `thread_id`、`run_id` 和 `web_thread_link`;其中 `--message` 为必填参数。
|
|
205
205
|
`+get-thread`: 请求中带 `version=v2`,并输出 `readable_text`。
|
|
206
206
|
`+upload-file`: 输出返回的 `asset_id`。 当前仅支持 `.doc`、`.docx` 和 `.txt` 文件。
|
|
207
|
-
`+download-result`: 会把结果 URL 下载到 `--output-path`
|
|
207
|
+
`+download-result`: 会把结果 URL 下载到 `--output-path` 指定的文件路径;传入 `--updated-at` 后,如果本地文件早于该时间戳会覆盖更新,否则跳过。
|
|
208
208
|
|
|
209
209
|
短剧命令的错误日志会追加写入本地每日日志文件:`~/.pippit_tool_cli/logs/yyyy-mm-dd.log`。日志路径会基于当前用户主目录和系统路径分隔符生成,因此可在 macOS、Linux 和 Windows 上使用。
|
|
210
210
|
|
package/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
2de4679cc459fe3c4d674fb25f00c8df8fa84e98f80a2fd01f5357d07c663c3e pippit-tool-cli-0.0.22-darwin-amd64.tar.gz
|
|
2
|
+
9a63d12f5f0534994c01f57caba16ec0d31a141afbe342cf8f42f7e954270633 pippit-tool-cli-0.0.22-darwin-arm64.tar.gz
|
|
3
|
+
61d20c1df72d556b0bd7157c77cdfe3e4c91bc0d85d8924efb0d49c4d6b36ef2 pippit-tool-cli-0.0.22-linux-amd64.tar.gz
|
|
4
|
+
76407ca98a7f885e06cab745eb09a8e5225bbeaae2226ad4c63c3c403bbdec5d pippit-tool-cli-0.0.22-linux-arm64.tar.gz
|
|
5
|
+
3db90d5c2b5a6a0ba964dd51424ffbc0eaf6dffb3497b5be226d16534504e8e6 pippit-tool-cli-0.0.22-windows-amd64.zip
|
|
6
|
+
b3a69cb5e43a21c037e30e5f6af53c1806de5f24e0724ddbbcbd005255cd20ce pippit-tool-cli-0.0.22-windows-arm64.zip
|
|
@@ -104,11 +104,15 @@ func newShortDramaDownloadResultCommand(stdout, stderr io.Writer, runner *common
|
|
|
104
104
|
Short: "Download a generated result URL",
|
|
105
105
|
Args: cobra.NoArgs,
|
|
106
106
|
RunE: withErrorLog("short-drama +download-result", func() map[string]string {
|
|
107
|
-
|
|
107
|
+
fields := map[string]string{
|
|
108
108
|
"output_path": opts.OutputPath,
|
|
109
109
|
"has_url": strconv.FormatBool(strings.TrimSpace(opts.URL) != ""),
|
|
110
110
|
"workers": strconv.Itoa(opts.Workers),
|
|
111
111
|
}
|
|
112
|
+
if opts.UpdatedAt > 0 {
|
|
113
|
+
fields["updated_at"] = strconv.FormatInt(opts.UpdatedAt, 10)
|
|
114
|
+
}
|
|
115
|
+
return fields
|
|
112
116
|
}, func(cmd *cobra.Command, _ []string) error {
|
|
113
117
|
opts.OutputPath = strings.TrimSpace(opts.OutputPath)
|
|
114
118
|
if opts.OutputPath == "" {
|
|
@@ -133,6 +137,7 @@ func newShortDramaDownloadResultCommand(stdout, stderr io.Writer, runner *common
|
|
|
133
137
|
cmd.SetErr(stderr)
|
|
134
138
|
cmd.Flags().StringVar(&opts.URL, "url", "", "URL to download")
|
|
135
139
|
cmd.Flags().StringVar(&opts.OutputPath, "output-path", "", "local output file path")
|
|
140
|
+
cmd.Flags().Int64Var(&opts.UpdatedAt, "updated-at", 0, "remote file update time as a Unix timestamp")
|
|
136
141
|
cmd.Flags().IntVar(&opts.Workers, "workers", 5, "parallel download workers")
|
|
137
142
|
return cmd
|
|
138
143
|
}
|
package/cmd/short_drama_test.go
CHANGED
|
@@ -408,6 +408,111 @@ func TestShortDramaDownloadResultSkipsExistingFile(t *testing.T) {
|
|
|
408
408
|
assertFileContent(t, outputPath, "existing-data")
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
+
func TestShortDramaDownloadResultSkipsExistingFileWhenLocalIsFresh(t *testing.T) {
|
|
412
|
+
serverCalled := false
|
|
413
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
414
|
+
serverCalled = true
|
|
415
|
+
t.Fatal("server should not receive request when target file is fresh")
|
|
416
|
+
}))
|
|
417
|
+
defer server.Close()
|
|
418
|
+
|
|
419
|
+
chdirTemp(t)
|
|
420
|
+
outputPath := filepath.Join("results", "cover.jpeg")
|
|
421
|
+
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
|
|
422
|
+
t.Fatalf("MkdirAll(): %v", err)
|
|
423
|
+
}
|
|
424
|
+
if err := os.WriteFile(outputPath, []byte("existing-data"), 0o644); err != nil {
|
|
425
|
+
t.Fatalf("WriteFile(): %v", err)
|
|
426
|
+
}
|
|
427
|
+
remoteUpdatedAt := int64(1779716734)
|
|
428
|
+
localUpdatedAt := time.Unix(remoteUpdatedAt+10, 0)
|
|
429
|
+
if err := os.Chtimes(outputPath, localUpdatedAt, localUpdatedAt); err != nil {
|
|
430
|
+
t.Fatalf("Chtimes(): %v", err)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
var stdout, stderr bytes.Buffer
|
|
434
|
+
root := NewRootCommand(&stdout, &stderr)
|
|
435
|
+
root.SetArgs([]string{
|
|
436
|
+
"short-drama", "+download-result",
|
|
437
|
+
"--output-path", outputPath,
|
|
438
|
+
"--updated-at", "1779716734",
|
|
439
|
+
"--url", server.URL + "/image",
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
if err := root.Execute(); err != nil {
|
|
443
|
+
t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
|
|
444
|
+
}
|
|
445
|
+
if serverCalled {
|
|
446
|
+
t.Fatal("server was called, want fresh existing file to skip download")
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
got := decodeJSON(t, stdout.Bytes())
|
|
450
|
+
alreadyExist, ok := got["already_exist"].([]any)
|
|
451
|
+
if !ok || len(alreadyExist) != 1 || alreadyExist[0] != outputPath {
|
|
452
|
+
t.Fatalf("already_exist = %#v, want existing output path", got["already_exist"])
|
|
453
|
+
}
|
|
454
|
+
assertFileContent(t, outputPath, "existing-data")
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
func TestShortDramaDownloadResultOverwritesStaleExistingFile(t *testing.T) {
|
|
458
|
+
serverCalled := false
|
|
459
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
460
|
+
serverCalled = true
|
|
461
|
+
if r.URL.Path != "/image" {
|
|
462
|
+
t.Fatalf("path = %s, want /image", r.URL.Path)
|
|
463
|
+
}
|
|
464
|
+
_, _ = w.Write([]byte("new-data"))
|
|
465
|
+
}))
|
|
466
|
+
defer server.Close()
|
|
467
|
+
|
|
468
|
+
chdirTemp(t)
|
|
469
|
+
outputPath := filepath.Join("results", "cover.jpeg")
|
|
470
|
+
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
|
|
471
|
+
t.Fatalf("MkdirAll(): %v", err)
|
|
472
|
+
}
|
|
473
|
+
if err := os.WriteFile(outputPath, []byte("existing-data"), 0o644); err != nil {
|
|
474
|
+
t.Fatalf("WriteFile(): %v", err)
|
|
475
|
+
}
|
|
476
|
+
remoteUpdatedAt := int64(1779716734)
|
|
477
|
+
staleUpdatedAt := time.Unix(remoteUpdatedAt-10, 0)
|
|
478
|
+
if err := os.Chtimes(outputPath, staleUpdatedAt, staleUpdatedAt); err != nil {
|
|
479
|
+
t.Fatalf("Chtimes(): %v", err)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
var stdout, stderr bytes.Buffer
|
|
483
|
+
root := NewRootCommand(&stdout, &stderr)
|
|
484
|
+
root.SetArgs([]string{
|
|
485
|
+
"short-drama", "+download-result",
|
|
486
|
+
"--output-path", outputPath,
|
|
487
|
+
"--updated-at", "1779716734",
|
|
488
|
+
"--url", server.URL + "/image",
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
if err := root.Execute(); err != nil {
|
|
492
|
+
t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
|
|
493
|
+
}
|
|
494
|
+
if !serverCalled {
|
|
495
|
+
t.Fatal("server was not called, want stale existing file to be downloaded")
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
got := decodeJSON(t, stdout.Bytes())
|
|
499
|
+
downloaded, ok := got["downloaded"].([]any)
|
|
500
|
+
if !ok || len(downloaded) != 1 || downloaded[0] != outputPath {
|
|
501
|
+
t.Fatalf("downloaded = %#v, want output path", got["downloaded"])
|
|
502
|
+
}
|
|
503
|
+
if _, ok := got["overwritten"]; ok {
|
|
504
|
+
t.Fatalf("overwritten should not be returned: %#v", got)
|
|
505
|
+
}
|
|
506
|
+
assertFileContent(t, outputPath, "new-data")
|
|
507
|
+
info, err := os.Stat(outputPath)
|
|
508
|
+
if err != nil {
|
|
509
|
+
t.Fatalf("Stat(%s): %v", outputPath, err)
|
|
510
|
+
}
|
|
511
|
+
if info.ModTime().Unix() != remoteUpdatedAt {
|
|
512
|
+
t.Fatalf("mtime = %d, want %d", info.ModTime().Unix(), remoteUpdatedAt)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
411
516
|
func TestShortDramaDownloadResultDownloadsMetaJSON(t *testing.T) {
|
|
412
517
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
413
518
|
if r.URL.Path != "/meta.json" {
|
|
@@ -513,6 +618,7 @@ func TestShortDramaDownloadResultAllFailed(t *testing.T) {
|
|
|
513
618
|
defer server.Close()
|
|
514
619
|
|
|
515
620
|
chdirTemp(t)
|
|
621
|
+
clearDailyErrorLog(t)
|
|
516
622
|
var stdout, stderr bytes.Buffer
|
|
517
623
|
root := NewRootCommand(&stdout, &stderr)
|
|
518
624
|
root.SetArgs([]string{
|
|
@@ -528,6 +634,17 @@ func TestShortDramaDownloadResultAllFailed(t *testing.T) {
|
|
|
528
634
|
if !strings.Contains(err.Error(), "all 1 download(s) failed") {
|
|
529
635
|
t.Fatalf("error = %q, want all-failed message", err)
|
|
530
636
|
}
|
|
637
|
+
logs := readDailyErrorLog(t)
|
|
638
|
+
if len(logs) != 1 {
|
|
639
|
+
t.Fatalf("logs = %#v, want one error log", logs)
|
|
640
|
+
}
|
|
641
|
+
fields, ok := logs[0]["fields"].(map[string]any)
|
|
642
|
+
if !ok {
|
|
643
|
+
t.Fatalf("fields = %#v, want object", logs[0]["fields"])
|
|
644
|
+
}
|
|
645
|
+
if _, ok := fields["updated_at"]; ok {
|
|
646
|
+
t.Fatalf("updated_at should be omitted when --updated-at is not provided: %#v", fields)
|
|
647
|
+
}
|
|
531
648
|
}
|
|
532
649
|
|
|
533
650
|
func TestShortDramaDownloadResultOutputPath(t *testing.T) {
|
|
@@ -684,7 +801,7 @@ func TestShortDramaListThreadFile(t *testing.T) {
|
|
|
684
801
|
if body["page_size"] != float64(10) {
|
|
685
802
|
t.Fatalf("page_size = %v, want 10", body["page_size"])
|
|
686
803
|
}
|
|
687
|
-
_, _ = w.Write([]byte(`{"ret":"0","errmsg":"","data":{"total":1,"files":[{"file_name":"ignored.png","file_path":"results/images/cover.png","download_url":"https://example.com/cover.png"}]}}`))
|
|
804
|
+
_, _ = w.Write([]byte(`{"ret":"0","errmsg":"","data":{"total":1,"files":[{"file_name":"ignored.png","file_path":"results/images/cover.png","download_url":"https://example.com/cover.png","updated_at":1779716734}]}}`))
|
|
688
805
|
}))
|
|
689
806
|
defer server.Close()
|
|
690
807
|
|
|
@@ -727,6 +844,9 @@ func TestShortDramaListThreadFile(t *testing.T) {
|
|
|
727
844
|
if file["download_url"] != "https://example.com/cover.png" {
|
|
728
845
|
t.Fatalf("download_url = %v, want returned url", file["download_url"])
|
|
729
846
|
}
|
|
847
|
+
if file["updated_at"] != float64(1779716734) {
|
|
848
|
+
t.Fatalf("updated_at = %v, want 1779716734", file["updated_at"])
|
|
849
|
+
}
|
|
730
850
|
}
|
|
731
851
|
|
|
732
852
|
func TestShortDramaListThreadFileMessageWhenPageFull(t *testing.T) {
|
|
@@ -23,6 +23,7 @@ import (
|
|
|
23
23
|
type DownloadResultOptions struct {
|
|
24
24
|
URL string `json:"url"`
|
|
25
25
|
OutputPath string `json:"output_path"`
|
|
26
|
+
UpdatedAt int64 `json:"updated_at,omitempty"`
|
|
26
27
|
Workers int `json:"workers"`
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -45,8 +46,9 @@ const (
|
|
|
45
46
|
)
|
|
46
47
|
|
|
47
48
|
type downloadTask struct {
|
|
48
|
-
url
|
|
49
|
-
filepath
|
|
49
|
+
url string
|
|
50
|
+
filepath string
|
|
51
|
+
updatedAt int64
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
type downloadTaskResult struct {
|
|
@@ -67,15 +69,16 @@ func DownloadResult(ctx context.Context, opts DownloadResultOptions, runner *Run
|
|
|
67
69
|
if outputPath == "" {
|
|
68
70
|
return nil, fmt.Errorf("output_path is required")
|
|
69
71
|
}
|
|
70
|
-
// check if the output path exists and is a file
|
|
71
72
|
if info, err := os.Stat(outputPath); err == nil {
|
|
72
73
|
if info.IsDir() {
|
|
73
74
|
return nil, fmt.Errorf("output path %q is a directory", outputPath)
|
|
74
75
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
if shouldSkipExistingFile(info, opts.UpdatedAt) {
|
|
77
|
+
return &DownloadResultResponse{
|
|
78
|
+
OutputPath: outputPath,
|
|
79
|
+
AlreadyExist: []string{outputPath},
|
|
80
|
+
}, nil
|
|
81
|
+
}
|
|
79
82
|
} else if !os.IsNotExist(err) {
|
|
80
83
|
return nil, fmt.Errorf("stat output path: %w", err)
|
|
81
84
|
}
|
|
@@ -93,8 +96,9 @@ func DownloadResult(ctx context.Context, opts DownloadResultOptions, runner *Run
|
|
|
93
96
|
}
|
|
94
97
|
tasks := []downloadTask{
|
|
95
98
|
{
|
|
96
|
-
url:
|
|
97
|
-
filepath:
|
|
99
|
+
url: rawURL,
|
|
100
|
+
filepath: outputPath,
|
|
101
|
+
updatedAt: opts.UpdatedAt,
|
|
98
102
|
},
|
|
99
103
|
}
|
|
100
104
|
|
|
@@ -117,7 +121,7 @@ func DownloadResult(ctx context.Context, opts DownloadResultOptions, runner *Run
|
|
|
117
121
|
for task := range taskCh {
|
|
118
122
|
resultCh <- downloadTaskResult{
|
|
119
123
|
filepath: task.filepath,
|
|
120
|
-
err: downloadFileWithRetry(ctx, runner.Client, task.url, task.filepath),
|
|
124
|
+
err: downloadFileWithRetry(ctx, runner.Client, task.url, task.filepath, task.updatedAt),
|
|
121
125
|
}
|
|
122
126
|
}
|
|
123
127
|
}()
|
|
@@ -167,7 +171,14 @@ func DownloadResult(ctx context.Context, opts DownloadResultOptions, runner *Run
|
|
|
167
171
|
return result, nil
|
|
168
172
|
}
|
|
169
173
|
|
|
170
|
-
func
|
|
174
|
+
func shouldSkipExistingFile(info os.FileInfo, updatedAt int64) bool {
|
|
175
|
+
if updatedAt <= 0 {
|
|
176
|
+
return true
|
|
177
|
+
}
|
|
178
|
+
return info.ModTime().Unix() >= updatedAt
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func downloadFileWithRetry(ctx context.Context, client Client, rawURL string, targetPath string, updatedAt int64) error {
|
|
171
182
|
var lastErr error
|
|
172
183
|
for attempt := 0; attempt <= maxDownloadRetries; attempt++ {
|
|
173
184
|
if attempt > 0 {
|
|
@@ -180,7 +191,7 @@ func downloadFileWithRetry(ctx context.Context, client Client, rawURL string, ta
|
|
|
180
191
|
case <-timer.C:
|
|
181
192
|
}
|
|
182
193
|
}
|
|
183
|
-
err := downloadFile(ctx, client, rawURL, targetPath)
|
|
194
|
+
err := downloadFile(ctx, client, rawURL, targetPath, updatedAt)
|
|
184
195
|
if err == nil {
|
|
185
196
|
return nil
|
|
186
197
|
}
|
|
@@ -203,7 +214,7 @@ func isRetryableError(err error) bool {
|
|
|
203
214
|
return false
|
|
204
215
|
}
|
|
205
216
|
|
|
206
|
-
func downloadFile(ctx context.Context, client Client, rawURL string, targetPath string) error {
|
|
217
|
+
func downloadFile(ctx context.Context, client Client, rawURL string, targetPath string, updatedAt int64) error {
|
|
207
218
|
var resp *http.Response
|
|
208
219
|
if err := client.SendRequest(ctx, rawURL, nil, &resp); err != nil {
|
|
209
220
|
return err
|
|
@@ -211,6 +222,12 @@ func downloadFile(ctx context.Context, client Client, rawURL string, targetPath
|
|
|
211
222
|
defer resp.Body.Close()
|
|
212
223
|
|
|
213
224
|
tmpPath := targetPath + ".tmp"
|
|
225
|
+
tempActive := true
|
|
226
|
+
defer func() {
|
|
227
|
+
if tempActive {
|
|
228
|
+
_ = os.Remove(tmpPath)
|
|
229
|
+
}
|
|
230
|
+
}()
|
|
214
231
|
out, err := os.Create(tmpPath)
|
|
215
232
|
if err != nil {
|
|
216
233
|
return fmt.Errorf("create temp file: %w", err)
|
|
@@ -218,16 +235,18 @@ func downloadFile(ctx context.Context, client Client, rawURL string, targetPath
|
|
|
218
235
|
_, copyErr := io.Copy(out, resp.Body)
|
|
219
236
|
closeErr := out.Close()
|
|
220
237
|
if copyErr != nil {
|
|
221
|
-
_ = os.Remove(tmpPath)
|
|
222
238
|
return fmt.Errorf("write temp file: %w", copyErr)
|
|
223
239
|
}
|
|
224
240
|
if closeErr != nil {
|
|
225
|
-
_ = os.Remove(tmpPath)
|
|
226
241
|
return fmt.Errorf("close temp file: %w", closeErr)
|
|
227
242
|
}
|
|
228
243
|
if err := os.Rename(tmpPath, targetPath); err != nil {
|
|
229
|
-
_ = os.Remove(tmpPath)
|
|
230
244
|
return fmt.Errorf("replace target file: %w", err)
|
|
231
245
|
}
|
|
246
|
+
tempActive = false
|
|
247
|
+
if updatedAt > 0 {
|
|
248
|
+
modTime := time.Unix(updatedAt, 0)
|
|
249
|
+
_ = os.Chtimes(targetPath, time.Now(), modTime)
|
|
250
|
+
}
|
|
232
251
|
return nil
|
|
233
252
|
}
|
|
@@ -22,6 +22,7 @@ type ListThreadFileOptions struct {
|
|
|
22
22
|
type ThreadFile struct {
|
|
23
23
|
FilePath string `json:"file_path"`
|
|
24
24
|
DownloadURL string `json:"download_url"`
|
|
25
|
+
UpdatedAt *int64 `json:"updated_at,omitempty"`
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
// ListThreadFileResult is the JSON envelope printed by `pippit-tool-cli short-drama +list-thread-file`.
|
|
@@ -45,6 +46,7 @@ type listThreadFileResponse struct {
|
|
|
45
46
|
type threadFileResponse struct {
|
|
46
47
|
FilePath string `json:"file_path"`
|
|
47
48
|
DownloadURL string `json:"download_url"`
|
|
49
|
+
UpdatedAt *int64 `json:"updated_at,omitempty"`
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
func ListThreadFile(ctx context.Context, opts *ListThreadFileOptions, runner *Runner) (*ListThreadFileResult, error) {
|
|
@@ -81,6 +83,7 @@ func ListThreadFile(ctx context.Context, opts *ListThreadFileOptions, runner *Ru
|
|
|
81
83
|
files = append(files, &ThreadFile{
|
|
82
84
|
FilePath: threadFilePath(opts.ThreadID, file.FilePath),
|
|
83
85
|
DownloadURL: file.DownloadURL,
|
|
86
|
+
UpdatedAt: file.UpdatedAt,
|
|
84
87
|
})
|
|
85
88
|
}
|
|
86
89
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pippit-dev/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"description": "Pippit CLI",
|
|
5
5
|
"bin": {
|
|
6
6
|
"pippit-tool-cli": "scripts/run.js"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
9
|
"postinstall": "node scripts/install.js",
|
|
10
|
-
"test": "go test ./... && go vet ./..."
|
|
10
|
+
"test": "node scripts/version-check.test.js && go test ./... && go vet ./..."
|
|
11
11
|
},
|
|
12
12
|
"os": [
|
|
13
13
|
"darwin",
|
package/scripts/version-check.js
CHANGED
|
@@ -7,7 +7,7 @@ const { DEFAULT_PKG } = require("./skills");
|
|
|
7
7
|
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
8
8
|
|
|
9
9
|
function defaultCacheFile() {
|
|
10
|
-
return path.join(os.homedir(), ".
|
|
10
|
+
return path.join(os.homedir(), ".pippit_tool_cli", "version-check.json");
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function currentVersion() {
|
|
@@ -91,6 +91,7 @@ function maybeWarnNewVersion(args = [], opts = {}) {
|
|
|
91
91
|
module.exports = {
|
|
92
92
|
CHECK_INTERVAL_MS,
|
|
93
93
|
compareSemver,
|
|
94
|
+
defaultCacheFile,
|
|
94
95
|
maybeWarnNewVersion,
|
|
95
96
|
parseSemver,
|
|
96
97
|
};
|
|
@@ -121,7 +121,8 @@ pippit-tool-cli short-drama +list-thread-file --thread-id THREAD_ID --page-num 1
|
|
|
121
121
|
```json
|
|
122
122
|
{
|
|
123
123
|
"file_path": "./{thread-id}/路径/文件名", // 文件完整路径,包含文件名
|
|
124
|
-
"download_url": "https://..." // URL
|
|
124
|
+
"download_url": "https://...", // URL
|
|
125
|
+
"updated_at": 1779716734 // 文件更新时间,Unix 秒级时间戳
|
|
125
126
|
}
|
|
126
127
|
```
|
|
127
128
|
|
|
@@ -131,10 +132,10 @@ pippit-tool-cli short-drama +list-thread-file --thread-id THREAD_ID --page-num 1
|
|
|
131
132
|
|
|
132
133
|
```bash
|
|
133
134
|
# 下载文件资源到指定文件路径
|
|
134
|
-
pippit-tool-cli short-drama +download-result --url DOWNLOAD_URL --output-path FILE_PATH
|
|
135
|
+
pippit-tool-cli short-drama +download-result --url DOWNLOAD_URL --output-path FILE_PATH --updated-at UPDATED_AT
|
|
135
136
|
```
|
|
136
137
|
|
|
137
|
-
`FILE_PATH` 必须直接使用 `+list-thread-file` 返回的完整 `file_path
|
|
138
|
+
`FILE_PATH` 必须直接使用 `+list-thread-file` 返回的完整 `file_path`,包含文件名,不要取父目录。`UPDATED_AT` 使用同一文件对象返回的 `updated_at`;如果没有 `updated_at`,可省略 `--updated-at`。`+download-result` 负责把会话产生的文件通过 URL 下载到该目标文件路径;如果目标文件已存在且本地修改时间不早于 `updated_at`,跳过下载;如果本地文件早于 `updated_at`,覆盖更新。
|
|
138
139
|
|
|
139
140
|
## 典型工作流
|
|
140
141
|
|
|
@@ -151,7 +152,7 @@ pippit-tool-cli short-drama +download-result --url DOWNLOAD_URL --output-path FI
|
|
|
151
152
|
- 如果任务仍在进行中:展示可读进展,继续查询
|
|
152
153
|
- 如果后端 Agent 提出问题:从 readable_text 中提取问题并展示,等待用户回复
|
|
153
154
|
5. 检查 `list-thread-file` 返回的 files:
|
|
154
|
-
- 对每个文件取 file_path、download_url
|
|
155
|
+
- 对每个文件取 file_path、download_url、updated_at
|
|
155
156
|
- 将 file_path 作为本地目标文件路径,包含文件名
|
|
156
157
|
- 有 download_url 的重要资产:加入本轮下载队列
|
|
157
158
|
- 不判断 file_path 在本地是否已存在,是否跳过由 +download-result 内部处理
|
|
@@ -159,6 +160,7 @@ pippit-tool-cli short-drama +download-result --url DOWNLOAD_URL --output-path FI
|
|
|
159
160
|
6. 对重要资产,立即调用 +download-result 并行下载资源:
|
|
160
161
|
- 使用第 5 步获取的 download_url 作为 --url
|
|
161
162
|
- 使用第 5 步获取的完整 file_path 作为 --output-path
|
|
163
|
+
- 如果第 5 步返回 updated_at,作为 --updated-at 传入
|
|
162
164
|
- 剧本设计、场景设计、场景图、人物角色设计、人物图、最终视频产物都属于重要资产
|
|
163
165
|
7. 查询或下载失败时,不要直接放弃;记录失败项,并在后续轮询中主动重试
|
|
164
166
|
8. 只有会话进展已处理,且已发现的重要资产均已下载或明确重试失败后,才向用户汇总最终结果
|
|
@@ -249,7 +251,8 @@ Thread: thread_...
|
|
|
249
251
|
"files": [
|
|
250
252
|
{
|
|
251
253
|
"file_path": "./{thread-id}/{file_path}/{file_name}",
|
|
252
|
-
"download_url": "https://..."
|
|
254
|
+
"download_url": "https://...",
|
|
255
|
+
"updated_at": 1779716734
|
|
253
256
|
}
|
|
254
257
|
],
|
|
255
258
|
"total": 1,
|
|
@@ -274,12 +277,12 @@ Thread: thread_...
|
|
|
274
277
|
|
|
275
278
|
### 获取会话文件
|
|
276
279
|
|
|
277
|
-
从 `+list-thread-file` 的 `files` 中逐个读取文件元信息:`file_path`、`file_name`、`download_url`。重点识别剧本设计、场景设计、场景图、人物角色设计、人物图、分集草稿、故事板、最终视频产物等重要资产。
|
|
280
|
+
从 `+list-thread-file` 的 `files` 中逐个读取文件元信息:`file_path`、`file_name`、`download_url`、`updated_at`。重点识别剧本设计、场景设计、场景图、人物角色设计、人物图、分集草稿、故事板、最终视频产物等重要资产。
|
|
278
281
|
|
|
279
282
|
```
|
|
280
283
|
1. 有download_url的重要资产
|
|
281
|
-
→ 记录该file_path和
|
|
282
|
-
→ 使用 +download-result 将URL资源下载到该file_path
|
|
284
|
+
→ 记录该file_path、URL和updated_at
|
|
285
|
+
→ 使用 +download-result 将URL资源下载到该file_path;有updated_at时传入--updated-at
|
|
283
286
|
2. 本轮total达到200
|
|
284
287
|
→ 下一轮page-num加1,继续查询新一页结果
|
|
285
288
|
3. 本轮total未达到200
|
|
@@ -293,7 +296,7 @@ Thread: thread_...
|
|
|
293
296
|
|
|
294
297
|
对带 `download_url` 的重要资产调用下载工具,可并行。重要资产必须主动下载,不要等用户再次要求,也不要在调用下载工具前先检查本地文件是否存在。
|
|
295
298
|
|
|
296
|
-
1. 调用 `pippit-tool-cli short-drama +download-result --url DOWNLOAD_URL --output-path FILE_PATH`。
|
|
299
|
+
1. 调用 `pippit-tool-cli short-drama +download-result --url DOWNLOAD_URL --output-path FILE_PATH --updated-at UPDATED_AT`;如果文件对象没有 `updated_at`,省略 `--updated-at`。
|
|
297
300
|
2. 下载完成后,向用户展示本地文件路径;如果某个文件下载失败,记录失败项并在后续轮询中重试,不阻塞已成功落盘的文件展示。
|
|
298
301
|
|
|
299
302
|
## 向用户展示内容
|
|
@@ -335,4 +338,4 @@ Thread: thread_...
|
|
|
335
338
|
- 单次创作会话中(相同 `thread_id`),`+submit-run` 只支持绑定一个剧本文件。不要在同一 `thread_id` 下重复上传并追加第二个剧本 `asset_id`;用户给多个剧本时,先让用户选择一个,或分别开启新的创作会话。
|
|
336
339
|
- `+list-thread-file` 只需要 `thread_id`;分页参数使用 `--page-num 1 --page-size 200` 起步,`total` 达到 200 时下一轮递增 `page-num`。
|
|
337
340
|
- `+list-thread-file` 和 `+download-result` 是两个不同的 CLI 指令:前者获取会话文件元信息,后者下载 URL 资源并写入到本地目标文件路径。
|
|
338
|
-
- `+download-result` 接收 `--url`、`--output-path`、`--workers`;`--output-path` 必须是包含文件名的目标文件路径。
|
|
341
|
+
- `+download-result` 接收 `--url`、`--output-path`、`--updated-at`、`--workers`;`--output-path` 必须是包含文件名的目标文件路径。
|