@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` 会查询指定 Run
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
440
|
-
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
39
|
-
State
|
|
40
|
-
|
|
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
|
|
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 !=
|
|
90
|
-
|
|
91
|
-
Completed:
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
144
|
+
actualOutputPath = download.Downloaded[0]
|
|
145
|
+
} else if len(download.AlreadyExist) > 0 {
|
|
146
|
+
actualOutputPath = download.AlreadyExist[0]
|
|
126
147
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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 == "" {
|