@pippit-dev/cli 1.0.2 → 1.0.4

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
@@ -238,7 +238,7 @@ pippit-tool-cli query-result \
238
238
  --download-dir "./output"
239
239
  ```
240
240
 
241
- `query-result` 会查询指定 RunRun 完成后下载视频产物并输出本地结果路径,未完成时输出当前状态和重试提示。
241
+ `query-result` 会查询指定 Run 并输出 JSON。Run 成功完成后下载视频产物,`completed=true`,`videos` 中只包含 `download_url` 和 `output_path`;Run 失败也视为终态,`completed=true` 且填充 `error_message`;Run 未到终态时 `completed=false`。
242
242
 
243
243
  ## HTTP 客户端
244
244
 
package/checksums.txt CHANGED
@@ -1,6 +1,6 @@
1
- 2674fbc4cf095e321dcba42a0e7ed44ecb50c14236a07c97dbc522227c8819e1 pippit-tool-cli-1.0.2-darwin-amd64.tar.gz
2
- f58a8babc317bef904699938b69574bb26d28cf191fff5123be305be77e27c3b pippit-tool-cli-1.0.2-darwin-arm64.tar.gz
3
- 3b33fbf6bc66f77b54e1436c5e231f355fe10787d41fe1310b6575b36e7c7cee pippit-tool-cli-1.0.2-linux-amd64.tar.gz
4
- f5e73a43c79d5d4a35872a05ad9d14c1260753042a004ef358c475e7e7c4e764 pippit-tool-cli-1.0.2-linux-arm64.tar.gz
5
- 36f48c0db0f6524d43fd49d2e2cea014166c9dad85834f1cd498c2e8ebfeafd4 pippit-tool-cli-1.0.2-windows-amd64.zip
6
- b45999f5e9412a2d7728e538b9cedbb715f4ffc397c667e610bcb964311db8f0 pippit-tool-cli-1.0.2-windows-arm64.zip
1
+ 889fabfbe3bd94cfeb34f9e3ee5fd9c2b39d5ab57f13e8b2c257dfca142bb2ce pippit-tool-cli-1.0.4-darwin-amd64.tar.gz
2
+ 8c54acc247b9f7050697fd2c52f3df84558fcfe01db3c7e186b4741294738236 pippit-tool-cli-1.0.4-darwin-arm64.tar.gz
3
+ 45de9614ed9b4241775f0451e603ab833484810def2eed31e4b3673a9cfe283d pippit-tool-cli-1.0.4-linux-amd64.tar.gz
4
+ 44852e1542b44085d543f2f2672ae9793e6e6bf7457ec90f7efcf46e3e252820 pippit-tool-cli-1.0.4-linux-arm64.tar.gz
5
+ 4b921069137fbe129973dbceae7fdbaa8a8f5cc2803ff3347b1e21452c3b9946 pippit-tool-cli-1.0.4-windows-amd64.zip
6
+ adf357fab99845e47933669d0fef8395189e023f69546454f61e4aca14392195 pippit-tool-cli-1.0.4-windows-arm64.zip
@@ -1,7 +1,7 @@
1
1
  package generate_video
2
2
 
3
3
  import (
4
- "fmt"
4
+ "encoding/json"
5
5
  "io"
6
6
  "strings"
7
7
 
@@ -26,27 +26,11 @@ func NewQueryResultCommand(stdout, stderr io.Writer, runner *common.Runner) *cob
26
26
  "run_id": strings.TrimSpace(opts.RunID),
27
27
  "download_dir": strings.TrimSpace(opts.DownloadDir),
28
28
  })
29
- return err
29
+ result = queryResultFromError(err, opts)
30
30
  }
31
- if !result.Completed {
32
- _, err = fmt.Fprintf(stdout, "Run 尚未完成,当前状态:%d\n请稍后重试 query-result。\n", result.State)
33
- return err
34
- }
35
- _, err = fmt.Fprintln(stdout, "Run 已完成,产物已下载:")
36
- if err != nil {
37
- return err
38
- }
39
- for i, path := range result.OutputPaths {
40
- if _, err := fmt.Fprintln(stdout, path); err != nil {
41
- return err
42
- }
43
- if i < len(result.DownloadURLs) && strings.TrimSpace(result.DownloadURLs[i]) != "" {
44
- if _, err := fmt.Fprintf(stdout, "download_url: %s\n", result.DownloadURLs[i]); err != nil {
45
- return err
46
- }
47
- }
48
- }
49
- return nil
31
+ encoder := json.NewEncoder(stdout)
32
+ encoder.SetIndent("", " ")
33
+ return encoder.Encode(result)
50
34
  },
51
35
  }
52
36
  cmd.SetOut(stdout)
@@ -56,3 +40,15 @@ func NewQueryResultCommand(stdout, stderr io.Writer, runner *common.Runner) *cob
56
40
  cmd.Flags().StringVar(&opts.DownloadDir, "download-dir", "", "directory to download completed videos into")
57
41
  return cmd
58
42
  }
43
+
44
+ func queryResultFromError(err error, opts *internalgen.QueryResultOptions) *internalgen.QueryResultResult {
45
+ result := &internalgen.QueryResultResult{
46
+ ErrorMessage: err.Error(),
47
+ Videos: []internalgen.QueryResultVideo{},
48
+ }
49
+ if opts != nil {
50
+ result.ThreadID = strings.TrimSpace(opts.ThreadID)
51
+ result.RunID = strings.TrimSpace(opts.RunID)
52
+ }
53
+ return result
54
+ }
@@ -332,10 +332,34 @@ func TestQueryResultDownloadsCompletedVideo(t *testing.T) {
332
332
  if !requestedDownload {
333
333
  t.Fatal("download endpoint was not requested")
334
334
  }
335
- if got := stdout.String(); !strings.Contains(got, "Run 已完成,产物已下载:") ||
336
- !strings.Contains(got, outputPath) ||
337
- !strings.Contains(got, "download_url: "+downloadURL) {
338
- t.Fatalf("stdout = %q, want download path and download url", got)
335
+ got := decodeJSON(t, stdout.Bytes())
336
+ if got["completed"] != true {
337
+ t.Fatalf("completed = %v, want true", got["completed"])
338
+ }
339
+ if got["thread_id"] != "thread_123" || got["run_id"] != "run_456" {
340
+ t.Fatalf("ids = (%v, %v), want thread/run ids", got["thread_id"], got["run_id"])
341
+ }
342
+ if _, ok := got["state"]; ok {
343
+ t.Fatalf("state should not be returned: %#v", got)
344
+ }
345
+ if got["error_message"] != "" {
346
+ t.Fatalf("error_message = %v, want empty", got["error_message"])
347
+ }
348
+ videos, ok := got["videos"].([]any)
349
+ if !ok || len(videos) != 1 {
350
+ t.Fatalf("videos = %#v, want one video", got["videos"])
351
+ }
352
+ video, ok := videos[0].(map[string]any)
353
+ if !ok {
354
+ t.Fatalf("video = %#v, want object", videos[0])
355
+ }
356
+ if video["download_url"] != downloadURL || video["output_path"] != outputPath {
357
+ t.Fatalf("video = %#v, want download_url/output_path", video)
358
+ }
359
+ for _, unwanted := range []string{"vid", "asset_id", "title"} {
360
+ if _, ok := video[unwanted]; ok {
361
+ t.Fatalf("video = %#v, should not contain %s", video, unwanted)
362
+ }
339
363
  }
340
364
  assertFileContent(t, outputPath, "video-data")
341
365
  }
@@ -360,30 +384,64 @@ func TestQueryResultIgnoresVideoDataWithoutVideoSubType(t *testing.T) {
360
384
  "--download-dir", t.TempDir(),
361
385
  })
362
386
 
363
- err := root.Execute()
364
- if err == nil {
365
- t.Fatal("Execute() error = nil, want no downloadable video error")
387
+ if err := root.Execute(); err != nil {
388
+ t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
389
+ }
390
+ got := decodeJSON(t, stdout.Bytes())
391
+ if got["completed"] != false {
392
+ t.Fatalf("completed = %v, want false", got["completed"])
393
+ }
394
+ if got["thread_id"] != "thread_123" || got["run_id"] != "run_456" {
395
+ t.Fatalf("ids = (%v, %v), want thread/run ids", got["thread_id"], got["run_id"])
366
396
  }
367
- if !strings.Contains(err.Error(), "下载失败:未找到可下载的视频产物") {
368
- t.Fatalf("error = %q, want no downloadable video error", err)
397
+ if got["error_message"] != "下载失败:未找到可下载的视频产物" {
398
+ t.Fatalf("error_message = %v, want no downloadable video error", got["error_message"])
369
399
  }
370
- if stdout.Len() != 0 {
371
- t.Fatalf("stdout = %q, want empty", stdout.String())
400
+ videos, ok := got["videos"].([]any)
401
+ if !ok || len(videos) != 0 {
402
+ t.Fatalf("videos = %#v, want empty", got["videos"])
372
403
  }
373
404
  }
374
405
 
375
- func TestQueryResultErrorLogIncludesLogID(t *testing.T) {
406
+ func TestQueryResultValidationErrorReturnsJSON(t *testing.T) {
407
+ var stdout, stderr bytes.Buffer
408
+ root := newTestRootCommand(t, &stdout, &stderr, "http://127.0.0.1")
409
+ root.SetArgs([]string{
410
+ "query-result",
411
+ "--run-id", "run_456",
412
+ "--download-dir", t.TempDir(),
413
+ })
414
+
415
+ if err := root.Execute(); err != nil {
416
+ t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
417
+ }
418
+ got := decodeJSON(t, stdout.Bytes())
419
+ if got["completed"] != false {
420
+ t.Fatalf("completed = %v, want false", got["completed"])
421
+ }
422
+ if got["thread_id"] != "" || got["run_id"] != "run_456" {
423
+ t.Fatalf("ids = (%v, %v), want empty thread_id and run_id", got["thread_id"], got["run_id"])
424
+ }
425
+ if got["error_message"] != "查询失败:缺少必填参数 --thread-id" {
426
+ t.Fatalf("error_message = %v, want validation error", got["error_message"])
427
+ }
428
+ videos, ok := got["videos"].([]any)
429
+ if !ok || len(videos) != 0 {
430
+ t.Fatalf("videos = %#v, want empty", got["videos"])
431
+ }
432
+ }
433
+
434
+ func TestQueryResultGetThreadBusinessErrorReturnsErrorMessage(t *testing.T) {
376
435
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
377
436
  switch r.URL.Path {
378
437
  case "/api/biz/v1/skill/get_thread":
379
- _, _ = w.Write([]byte(`{"ret":"5","errmsg":"创作失败","log_id":"log_456"}`))
438
+ _, _ = w.Write([]byte(`{"ret":"5","errmsg":"创作失败:暂时无法生成","log_id":"log_456"}`))
380
439
  default:
381
440
  t.Fatalf("unexpected path %s", r.URL.Path)
382
441
  }
383
442
  }))
384
443
  defer server.Close()
385
444
 
386
- clearDailyErrorLog(t)
387
445
  var stdout, stderr bytes.Buffer
388
446
  root := newTestRootCommand(t, &stdout, &stderr, server.URL)
389
447
  root.SetArgs([]string{
@@ -393,23 +451,61 @@ func TestQueryResultErrorLogIncludesLogID(t *testing.T) {
393
451
  "--download-dir", t.TempDir(),
394
452
  })
395
453
 
396
- err := root.Execute()
397
- if err == nil {
398
- t.Fatal("Execute() error = nil, want get_thread error")
454
+ if err := root.Execute(); err != nil {
455
+ t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
399
456
  }
400
- entries := readDailyErrorLog(t)
401
- if len(entries) != 1 {
402
- t.Fatalf("log entries = %d, want 1: %#v", len(entries), entries)
457
+ got := decodeJSON(t, stdout.Bytes())
458
+ if got["completed"] != true {
459
+ t.Fatalf("completed = %v, want true", got["completed"])
403
460
  }
404
- fields, ok := entries[0]["fields"].(map[string]any)
405
- if !ok {
406
- t.Fatalf("fields = %#v, want object", entries[0]["fields"])
461
+ if got["thread_id"] != "thread_123" || got["run_id"] != "run_456" {
462
+ t.Fatalf("ids = (%v, %v), want thread/run ids", got["thread_id"], got["run_id"])
463
+ }
464
+ if got["error_message"] != "创作失败:暂时无法生成 log_id=log_456" {
465
+ t.Fatalf("error_message = %v, want get_thread business error", got["error_message"])
466
+ }
467
+ videos, ok := got["videos"].([]any)
468
+ if !ok || len(videos) != 0 {
469
+ t.Fatalf("videos = %#v, want empty", got["videos"])
470
+ }
471
+ }
472
+
473
+ func TestQueryResultFailedReturnsErrorMessage(t *testing.T) {
474
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
475
+ switch r.URL.Path {
476
+ case "/api/biz/v1/skill/get_thread":
477
+ _, _ = w.Write([]byte(`{"ret":"0","errmsg":"","data":{"thread":{"thread_id":"thread_123","run_list":[{"run_id":"run_456","state":4,"entry_list":[{"artifact":{"content":[{"sub_type":"biz/x_data_video","data":"{\"error_message\":\"生成失败\",\"error_code\":\"11001\"}"}]}}]}]}}}`))
478
+ default:
479
+ t.Fatalf("unexpected path %s", r.URL.Path)
480
+ }
481
+ }))
482
+ defer server.Close()
483
+
484
+ var stdout, stderr bytes.Buffer
485
+ root := newTestRootCommand(t, &stdout, &stderr, server.URL)
486
+ root.SetArgs([]string{
487
+ "query-result",
488
+ "--thread-id", "thread_123",
489
+ "--run-id", "run_456",
490
+ "--download-dir", t.TempDir(),
491
+ })
492
+
493
+ if err := root.Execute(); err != nil {
494
+ t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
495
+ }
496
+ got := decodeJSON(t, stdout.Bytes())
497
+ if got["completed"] != true {
498
+ t.Fatalf("completed = %v, want true", got["completed"])
407
499
  }
408
- if fields["log_id"] != "log_456" {
409
- t.Fatalf("log_id = %v, want log_456", fields["log_id"])
500
+ if _, ok := got["state"]; ok {
501
+ t.Fatalf("state should not be returned: %#v", got)
410
502
  }
411
- if fields["thread_id"] != "thread_123" || fields["run_id"] != "run_456" {
412
- t.Fatalf("fields = %#v, want thread/run ids", fields)
503
+ if got["error_message"] != "生成失败 (error_code=11001)" {
504
+ t.Fatalf("error_message = %v, want failure message", got["error_message"])
505
+ }
506
+ videos, ok := got["videos"].([]any)
507
+ if !ok || len(videos) != 0 {
508
+ t.Fatalf("videos = %#v, want empty", got["videos"])
413
509
  }
414
510
  }
415
511
 
@@ -436,8 +532,19 @@ func TestQueryResultPendingDoesNotDownload(t *testing.T) {
436
532
  if err := root.Execute(); err != nil {
437
533
  t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
438
534
  }
439
- if got := stdout.String(); !strings.Contains(got, "Run 尚未完成,当前状态:1") {
440
- t.Fatalf("stdout = %q, want pending state", got)
535
+ got := decodeJSON(t, stdout.Bytes())
536
+ if got["completed"] != false {
537
+ t.Fatalf("completed = %v, want false", got["completed"])
538
+ }
539
+ if _, ok := got["state"]; ok {
540
+ t.Fatalf("state should not be returned: %#v", got)
541
+ }
542
+ if got["error_message"] != "" {
543
+ t.Fatalf("error_message = %v, want empty", got["error_message"])
544
+ }
545
+ videos, ok := got["videos"].([]any)
546
+ if !ok || len(videos) != 0 {
547
+ t.Fatalf("videos = %#v, want empty", got["videos"])
441
548
  }
442
549
  }
443
550
 
@@ -3,6 +3,7 @@ package generate_video
3
3
  import (
4
4
  "context"
5
5
  "encoding/json"
6
+ "errors"
6
7
  "fmt"
7
8
  "path/filepath"
8
9
  "strconv"
@@ -12,7 +13,10 @@ import (
12
13
  "github.com/Pippit-dev/pippit-cli/internal/common"
13
14
  )
14
15
 
15
- const completedRunState = 3
16
+ const (
17
+ successRunState = 3
18
+ failedRunState = 4
19
+ )
16
20
 
17
21
  // QueryResultOptions is the command-facing request shape for query-result.
18
22
  type QueryResultOptions struct {
@@ -23,10 +27,17 @@ type QueryResultOptions struct {
23
27
 
24
28
  // QueryResultResult describes the user-facing query-result outcome.
25
29
  type QueryResultResult struct {
26
- Completed bool
27
- State int
28
- OutputPaths []string
29
- DownloadURLs []string
30
+ Completed bool `json:"completed"`
31
+ ThreadID string `json:"thread_id"`
32
+ RunID string `json:"run_id"`
33
+ ErrorMessage string `json:"error_message"`
34
+ Videos []QueryResultVideo `json:"videos"`
35
+ }
36
+
37
+ // QueryResultVideo describes a downloaded video from query-result.
38
+ type QueryResultVideo struct {
39
+ DownloadURL string `json:"download_url"`
40
+ OutputPath string `json:"output_path"`
30
41
  }
31
42
 
32
43
  type queryThread struct {
@@ -35,9 +46,12 @@ type queryThread struct {
35
46
  }
36
47
 
37
48
  type queryRun struct {
38
- RunID string `json:"run_id"`
39
- State int `json:"state"`
40
- EntryList []queryEntry `json:"entry_list"`
49
+ RunID string `json:"run_id"`
50
+ State int `json:"state"`
51
+ ErrorMessage string `json:"error_message"`
52
+ ErrorMsg string `json:"error_msg"`
53
+ Errmsg string `json:"errmsg"`
54
+ EntryList []queryEntry `json:"entry_list"`
41
55
  }
42
56
 
43
57
  type queryEntry struct {
@@ -54,7 +68,9 @@ type queryContent struct {
54
68
  }
55
69
 
56
70
  type queryContentData struct {
57
- Video *queryVideo `json:"video"`
71
+ Video *queryVideo `json:"video"`
72
+ ErrorMessage string `json:"error_message"`
73
+ ErrorCode json.RawMessage `json:"error_code"`
58
74
  }
59
75
 
60
76
  type queryVideo struct {
@@ -74,6 +90,9 @@ func QueryResult(ctx context.Context, opts *QueryResultOptions, runner *common.R
74
90
  RunID: opts.RunID,
75
91
  }, runner)
76
92
  if err != nil {
93
+ if result, ok := queryResultFromGetThreadBusinessError(err, opts); ok {
94
+ return result, nil
95
+ }
77
96
  return nil, fmt.Errorf("查询失败:%w", err)
78
97
  }
79
98
 
@@ -86,11 +105,17 @@ func QueryResult(ctx context.Context, opts *QueryResultOptions, runner *common.R
86
105
  if !ok {
87
106
  return nil, fmt.Errorf("查询失败:未找到 run_id=%s 对应的 Run", opts.RunID)
88
107
  }
89
- if run.State != completedRunState {
90
- return &QueryResultResult{
91
- Completed: false,
92
- State: run.State,
93
- }, nil
108
+ if run.State != successRunState {
109
+ result := &QueryResultResult{
110
+ Completed: run.State == failedRunState,
111
+ ThreadID: firstNonEmpty(thread.ThreadID, opts.ThreadID),
112
+ RunID: opts.RunID,
113
+ Videos: []QueryResultVideo{},
114
+ }
115
+ if run.State == failedRunState {
116
+ result.ErrorMessage = firstNonEmpty(extractQueryErrorMessage(run), "Run 失败")
117
+ }
118
+ return result, nil
94
119
  }
95
120
 
96
121
  videos := extractQueryVideos(run)
@@ -103,14 +128,12 @@ func QueryResult(ctx context.Context, opts *QueryResultOptions, runner *common.R
103
128
  return nil, fmt.Errorf("下载失败:解析下载目录失败:%w", err)
104
129
  }
105
130
 
106
- outputPaths := make([]string, 0, len(videos))
107
- downloadURLs := make([]string, 0, len(videos))
131
+ resultVideos := make([]QueryResultVideo, 0, len(videos))
108
132
  usedNames := make(map[string]int, len(videos))
109
133
  for i, video := range videos {
110
134
  if strings.TrimSpace(video.DownloadURL) == "" {
111
135
  return nil, fmt.Errorf("下载失败:第 %d 个视频产物 download_url 为空", i+1)
112
136
  }
113
- downloadURLs = append(downloadURLs, video.DownloadURL)
114
137
  outputPath := filepath.Join(downloadDir, uniqueQueryResultFileName(videoFileName(video, i+1), usedNames))
115
138
  download, err := common.DownloadResult(ctx, common.DownloadResultOptions{
116
139
  URL: video.DownloadURL,
@@ -120,25 +143,55 @@ func QueryResult(ctx context.Context, opts *QueryResultOptions, runner *common.R
120
143
  if err != nil {
121
144
  return nil, fmt.Errorf("下载失败:%w", err)
122
145
  }
146
+ actualOutputPath := outputPath
123
147
  if len(download.Downloaded) > 0 {
124
- outputPaths = append(outputPaths, download.Downloaded...)
125
- continue
148
+ actualOutputPath = download.Downloaded[0]
149
+ } else if len(download.AlreadyExist) > 0 {
150
+ actualOutputPath = download.AlreadyExist[0]
126
151
  }
127
- if len(download.AlreadyExist) > 0 {
128
- outputPaths = append(outputPaths, download.AlreadyExist...)
129
- continue
130
- }
131
- outputPaths = append(outputPaths, outputPath)
152
+ resultVideos = append(resultVideos, QueryResultVideo{
153
+ DownloadURL: video.DownloadURL,
154
+ OutputPath: actualOutputPath,
155
+ })
132
156
  }
133
157
 
134
158
  return &QueryResultResult{
135
- Completed: true,
136
- State: run.State,
137
- OutputPaths: outputPaths,
138
- DownloadURLs: downloadURLs,
159
+ Completed: true,
160
+ ThreadID: firstNonEmpty(thread.ThreadID, opts.ThreadID),
161
+ RunID: opts.RunID,
162
+ Videos: resultVideos,
139
163
  }, nil
140
164
  }
141
165
 
166
+ func queryResultFromGetThreadBusinessError(err error, opts *QueryResultOptions) (*QueryResultResult, bool) {
167
+ var logErr *common.LogIDError
168
+ if !errors.As(err, &logErr) {
169
+ return nil, false
170
+ }
171
+ message := getThreadBusinessErrorMessage(logErr.Message)
172
+ if message == "" {
173
+ message = "查询失败"
174
+ }
175
+ if logID := logErr.LogID(); logID != "" {
176
+ message = fmt.Sprintf("%s log_id=%s", message, logID)
177
+ }
178
+ return &QueryResultResult{
179
+ Completed: true,
180
+ ThreadID: opts.ThreadID,
181
+ RunID: opts.RunID,
182
+ ErrorMessage: message,
183
+ Videos: []QueryResultVideo{},
184
+ }, true
185
+ }
186
+
187
+ func getThreadBusinessErrorMessage(message string) string {
188
+ message = strings.TrimSpace(message)
189
+ if idx := strings.Index(message, "errmsg="); idx >= 0 {
190
+ return strings.TrimSpace(message[idx+len("errmsg="):])
191
+ }
192
+ return message
193
+ }
194
+
142
195
  func validateQueryResultOptions(opts *QueryResultOptions) error {
143
196
  if opts == nil {
144
197
  return fmt.Errorf("查询失败:缺少必填参数 --thread-id")
@@ -235,6 +288,39 @@ func extractQueryVideos(run queryRun) []queryVideo {
235
288
  return videos
236
289
  }
237
290
 
291
+ func extractQueryErrorMessage(run queryRun) string {
292
+ if message := firstNonEmpty(run.ErrorMessage, run.ErrorMsg, run.Errmsg); message != "" {
293
+ return message
294
+ }
295
+ for _, entry := range run.EntryList {
296
+ for _, content := range entry.Artifact.Content {
297
+ data := content.Data
298
+ if message := firstNonEmpty(data.ErrorMessage); message != "" {
299
+ if code := rawMessageString(data.ErrorCode); code != "" {
300
+ return fmt.Sprintf("%s (error_code=%s)", message, code)
301
+ }
302
+ return message
303
+ }
304
+ if code := rawMessageString(data.ErrorCode); code != "" {
305
+ return "error_code=" + code
306
+ }
307
+ }
308
+ }
309
+ return ""
310
+ }
311
+
312
+ func rawMessageString(raw json.RawMessage) string {
313
+ raw = json.RawMessage(strings.TrimSpace(string(raw)))
314
+ if len(raw) == 0 || string(raw) == "null" {
315
+ return ""
316
+ }
317
+ var value string
318
+ if err := json.Unmarshal(raw, &value); err == nil {
319
+ return strings.TrimSpace(value)
320
+ }
321
+ return strings.TrimSpace(string(raw))
322
+ }
323
+
238
324
  func videoFileName(video queryVideo, index int) string {
239
325
  name := firstNonEmpty(video.VID, video.Title, video.AssetID)
240
326
  if name == "" {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pippit-dev/cli",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Pippit CLI",
5
5
  "bin": {
6
6
  "pippit-tool-cli": "scripts/run.js"