@pippit-dev/cli 1.0.2 → 1.0.3

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
+ e344cebed19cb97f0aef66df54118cb7425714d614f2488c1072ff72d24d91cf pippit-tool-cli-1.0.3-darwin-amd64.tar.gz
2
+ a1a516570d4553ef0514442f938da11f0147ad524c9fa7fb078a1ab5d1d0e27d pippit-tool-cli-1.0.3-darwin-arm64.tar.gz
3
+ b7840c3b3d6cab08d74de8af15a88f09e9bc5f7c98cc14f7d80487edad505bfe pippit-tool-cli-1.0.3-linux-amd64.tar.gz
4
+ 9ef478103cf1e04d825da9a9e1065e76fec6e8e79407fa4543c8010c54ca1bb6 pippit-tool-cli-1.0.3-linux-arm64.tar.gz
5
+ f691764e9d5be6fe2d05b2039318458bb1c5763c5e17060389ba39d2b358f2b6 pippit-tool-cli-1.0.3-windows-amd64.zip
6
+ 58899eafda91b4d1c9768d3aaa8d1d04279b7f7f7c2a18541a9873da33882960 pippit-tool-cli-1.0.3-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
 
@@ -28,25 +28,9 @@ func NewQueryResultCommand(stdout, stderr io.Writer, runner *common.Runner) *cob
28
28
  })
29
29
  return err
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)
@@ -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
  }
@@ -413,6 +437,45 @@ func TestQueryResultErrorLogIncludesLogID(t *testing.T) {
413
437
  }
414
438
  }
415
439
 
440
+ func TestQueryResultFailedReturnsErrorMessage(t *testing.T) {
441
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
442
+ switch r.URL.Path {
443
+ case "/api/biz/v1/skill/get_thread":
444
+ _, _ = 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\"}"}]}}]}]}}}`))
445
+ default:
446
+ t.Fatalf("unexpected path %s", r.URL.Path)
447
+ }
448
+ }))
449
+ defer server.Close()
450
+
451
+ var stdout, stderr bytes.Buffer
452
+ root := newTestRootCommand(t, &stdout, &stderr, server.URL)
453
+ root.SetArgs([]string{
454
+ "query-result",
455
+ "--thread-id", "thread_123",
456
+ "--run-id", "run_456",
457
+ "--download-dir", t.TempDir(),
458
+ })
459
+
460
+ if err := root.Execute(); err != nil {
461
+ t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
462
+ }
463
+ got := decodeJSON(t, stdout.Bytes())
464
+ if got["completed"] != true {
465
+ t.Fatalf("completed = %v, want true", got["completed"])
466
+ }
467
+ if _, ok := got["state"]; ok {
468
+ t.Fatalf("state should not be returned: %#v", got)
469
+ }
470
+ if got["error_message"] != "生成失败 (error_code=11001)" {
471
+ t.Fatalf("error_message = %v, want failure message", got["error_message"])
472
+ }
473
+ videos, ok := got["videos"].([]any)
474
+ if !ok || len(videos) != 0 {
475
+ t.Fatalf("videos = %#v, want empty", got["videos"])
476
+ }
477
+ }
478
+
416
479
  func TestQueryResultPendingDoesNotDownload(t *testing.T) {
417
480
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
418
481
  switch r.URL.Path {
@@ -436,8 +499,19 @@ func TestQueryResultPendingDoesNotDownload(t *testing.T) {
436
499
  if err := root.Execute(); err != nil {
437
500
  t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
438
501
  }
439
- if got := stdout.String(); !strings.Contains(got, "Run 尚未完成,当前状态:1") {
440
- t.Fatalf("stdout = %q, want pending state", got)
502
+ got := decodeJSON(t, stdout.Bytes())
503
+ if got["completed"] != false {
504
+ t.Fatalf("completed = %v, want false", got["completed"])
505
+ }
506
+ if _, ok := got["state"]; ok {
507
+ t.Fatalf("state should not be returned: %#v", got)
508
+ }
509
+ if got["error_message"] != "" {
510
+ t.Fatalf("error_message = %v, want empty", got["error_message"])
511
+ }
512
+ videos, ok := got["videos"].([]any)
513
+ if !ok || len(videos) != 0 {
514
+ t.Fatalf("videos = %#v, want empty", got["videos"])
441
515
  }
442
516
  }
443
517
 
@@ -12,7 +12,10 @@ import (
12
12
  "github.com/Pippit-dev/pippit-cli/internal/common"
13
13
  )
14
14
 
15
- const completedRunState = 3
15
+ const (
16
+ successRunState = 3
17
+ failedRunState = 4
18
+ )
16
19
 
17
20
  // QueryResultOptions is the command-facing request shape for query-result.
18
21
  type QueryResultOptions struct {
@@ -23,10 +26,17 @@ type QueryResultOptions struct {
23
26
 
24
27
  // QueryResultResult describes the user-facing query-result outcome.
25
28
  type QueryResultResult struct {
26
- Completed bool
27
- State int
28
- OutputPaths []string
29
- DownloadURLs []string
29
+ Completed bool `json:"completed"`
30
+ ThreadID string `json:"thread_id"`
31
+ RunID string `json:"run_id"`
32
+ ErrorMessage string `json:"error_message"`
33
+ Videos []QueryResultVideo `json:"videos"`
34
+ }
35
+
36
+ // QueryResultVideo describes a downloaded video from query-result.
37
+ type QueryResultVideo struct {
38
+ DownloadURL string `json:"download_url"`
39
+ OutputPath string `json:"output_path"`
30
40
  }
31
41
 
32
42
  type queryThread struct {
@@ -35,9 +45,12 @@ type queryThread struct {
35
45
  }
36
46
 
37
47
  type queryRun struct {
38
- RunID string `json:"run_id"`
39
- State int `json:"state"`
40
- EntryList []queryEntry `json:"entry_list"`
48
+ RunID string `json:"run_id"`
49
+ State int `json:"state"`
50
+ ErrorMessage string `json:"error_message"`
51
+ ErrorMsg string `json:"error_msg"`
52
+ Errmsg string `json:"errmsg"`
53
+ EntryList []queryEntry `json:"entry_list"`
41
54
  }
42
55
 
43
56
  type queryEntry struct {
@@ -54,7 +67,9 @@ type queryContent struct {
54
67
  }
55
68
 
56
69
  type queryContentData struct {
57
- Video *queryVideo `json:"video"`
70
+ Video *queryVideo `json:"video"`
71
+ ErrorMessage string `json:"error_message"`
72
+ ErrorCode json.RawMessage `json:"error_code"`
58
73
  }
59
74
 
60
75
  type queryVideo struct {
@@ -86,11 +101,17 @@ func QueryResult(ctx context.Context, opts *QueryResultOptions, runner *common.R
86
101
  if !ok {
87
102
  return nil, fmt.Errorf("查询失败:未找到 run_id=%s 对应的 Run", opts.RunID)
88
103
  }
89
- if run.State != completedRunState {
90
- return &QueryResultResult{
91
- Completed: false,
92
- State: run.State,
93
- }, nil
104
+ if run.State != successRunState {
105
+ result := &QueryResultResult{
106
+ Completed: run.State == failedRunState,
107
+ ThreadID: firstNonEmpty(thread.ThreadID, opts.ThreadID),
108
+ RunID: opts.RunID,
109
+ Videos: []QueryResultVideo{},
110
+ }
111
+ if run.State == failedRunState {
112
+ result.ErrorMessage = firstNonEmpty(extractQueryErrorMessage(run), "Run 失败")
113
+ }
114
+ return result, nil
94
115
  }
95
116
 
96
117
  videos := extractQueryVideos(run)
@@ -103,14 +124,12 @@ func QueryResult(ctx context.Context, opts *QueryResultOptions, runner *common.R
103
124
  return nil, fmt.Errorf("下载失败:解析下载目录失败:%w", err)
104
125
  }
105
126
 
106
- outputPaths := make([]string, 0, len(videos))
107
- downloadURLs := make([]string, 0, len(videos))
127
+ resultVideos := make([]QueryResultVideo, 0, len(videos))
108
128
  usedNames := make(map[string]int, len(videos))
109
129
  for i, video := range videos {
110
130
  if strings.TrimSpace(video.DownloadURL) == "" {
111
131
  return nil, fmt.Errorf("下载失败:第 %d 个视频产物 download_url 为空", i+1)
112
132
  }
113
- downloadURLs = append(downloadURLs, video.DownloadURL)
114
133
  outputPath := filepath.Join(downloadDir, uniqueQueryResultFileName(videoFileName(video, i+1), usedNames))
115
134
  download, err := common.DownloadResult(ctx, common.DownloadResultOptions{
116
135
  URL: video.DownloadURL,
@@ -120,22 +139,23 @@ func QueryResult(ctx context.Context, opts *QueryResultOptions, runner *common.R
120
139
  if err != nil {
121
140
  return nil, fmt.Errorf("下载失败:%w", err)
122
141
  }
142
+ actualOutputPath := outputPath
123
143
  if len(download.Downloaded) > 0 {
124
- outputPaths = append(outputPaths, download.Downloaded...)
125
- continue
144
+ actualOutputPath = download.Downloaded[0]
145
+ } else if len(download.AlreadyExist) > 0 {
146
+ actualOutputPath = download.AlreadyExist[0]
126
147
  }
127
- if len(download.AlreadyExist) > 0 {
128
- outputPaths = append(outputPaths, download.AlreadyExist...)
129
- continue
130
- }
131
- outputPaths = append(outputPaths, outputPath)
148
+ resultVideos = append(resultVideos, QueryResultVideo{
149
+ DownloadURL: video.DownloadURL,
150
+ OutputPath: actualOutputPath,
151
+ })
132
152
  }
133
153
 
134
154
  return &QueryResultResult{
135
- Completed: true,
136
- State: run.State,
137
- OutputPaths: outputPaths,
138
- DownloadURLs: downloadURLs,
155
+ Completed: true,
156
+ ThreadID: firstNonEmpty(thread.ThreadID, opts.ThreadID),
157
+ RunID: opts.RunID,
158
+ Videos: resultVideos,
139
159
  }, nil
140
160
  }
141
161
 
@@ -235,6 +255,39 @@ func extractQueryVideos(run queryRun) []queryVideo {
235
255
  return videos
236
256
  }
237
257
 
258
+ func extractQueryErrorMessage(run queryRun) string {
259
+ if message := firstNonEmpty(run.ErrorMessage, run.ErrorMsg, run.Errmsg); message != "" {
260
+ return message
261
+ }
262
+ for _, entry := range run.EntryList {
263
+ for _, content := range entry.Artifact.Content {
264
+ data := content.Data
265
+ if message := firstNonEmpty(data.ErrorMessage); message != "" {
266
+ if code := rawMessageString(data.ErrorCode); code != "" {
267
+ return fmt.Sprintf("%s (error_code=%s)", message, code)
268
+ }
269
+ return message
270
+ }
271
+ if code := rawMessageString(data.ErrorCode); code != "" {
272
+ return "error_code=" + code
273
+ }
274
+ }
275
+ }
276
+ return ""
277
+ }
278
+
279
+ func rawMessageString(raw json.RawMessage) string {
280
+ raw = json.RawMessage(strings.TrimSpace(string(raw)))
281
+ if len(raw) == 0 || string(raw) == "null" {
282
+ return ""
283
+ }
284
+ var value string
285
+ if err := json.Unmarshal(raw, &value); err == nil {
286
+ return strings.TrimSpace(value)
287
+ }
288
+ return strings.TrimSpace(string(raw))
289
+ }
290
+
238
291
  func videoFileName(video queryVideo, index int) string {
239
292
  name := firstNonEmpty(video.VID, video.Title, video.AssetID)
240
293
  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.3",
4
4
  "description": "Pippit CLI",
5
5
  "bin": {
6
6
  "pippit-tool-cli": "scripts/run.js"