@pippit-dev/cli 1.0.0 → 1.0.2
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 +37 -5
- package/checksums.txt +6 -6
- package/cmd/download_result.go +57 -0
- package/cmd/generate_video/generate_video.go +48 -0
- package/cmd/generate_video/query_result.go +58 -0
- package/cmd/generate_video_test.go +468 -0
- package/cmd/get_thread.go +59 -0
- package/cmd/list_thread_file.go +51 -0
- package/cmd/root.go +30 -1
- package/cmd/short_drama/short_drama.go +12 -133
- package/cmd/short_drama_test.go +76 -49
- package/cmd/update/update.go +8 -8
- package/internal/common/client.go +29 -21
- package/internal/common/download_results.go +21 -15
- package/internal/common/error_log.go +62 -1
- package/internal/common/error_log_test.go +33 -3
- package/internal/common/get_thread.go +29 -15
- package/internal/common/get_thread_test.go +74 -0
- package/internal/common/json.go +18 -0
- package/internal/common/list_thread_file.go +7 -7
- package/internal/common/submit_run.go +29 -0
- package/internal/common/upload_file.go +22 -18
- package/internal/config/config.go +1 -1
- package/internal/generate_video/generate_video.go +199 -0
- package/internal/generate_video/query_result.go +291 -0
- package/internal/generate_video/query_result_test.go +31 -0
- package/internal/short_drama/submit_run.go +8 -28
- package/package.json +1 -1
- package/skills/short-drama/SKILL.md +34 -34
- package/skills/xyq-nest-skill/scripts/download_results.py +3 -1
- package/skills/xyq-nest-skill/scripts/upload_file.py +2 -2
- package/skills/xyq-nest-skill/scripts/xyq_common.py +4 -3
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"io"
|
|
6
|
+
"net/http"
|
|
7
|
+
"net/http/httptest"
|
|
8
|
+
"os"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"strings"
|
|
11
|
+
"testing"
|
|
12
|
+
|
|
13
|
+
"github.com/bytedance/sonic"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
func TestGenerateVideo(t *testing.T) {
|
|
17
|
+
assetIDs := []string{"image_asset_1", "image_asset_2", "video_asset_1", "video_asset_2"}
|
|
18
|
+
uploadIndex := 0
|
|
19
|
+
|
|
20
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
21
|
+
if r.Header.Get("Authorization") != "Bearer test-token" {
|
|
22
|
+
t.Fatalf("Authorization = %q, want test bearer token", r.Header.Get("Authorization"))
|
|
23
|
+
}
|
|
24
|
+
switch r.URL.Path {
|
|
25
|
+
case "/api/biz/v1/skill/upload_file":
|
|
26
|
+
if r.Method != http.MethodPost {
|
|
27
|
+
t.Fatalf("upload method = %s, want POST", r.Method)
|
|
28
|
+
}
|
|
29
|
+
if uploadIndex >= len(assetIDs) {
|
|
30
|
+
t.Fatalf("unexpected upload %d", uploadIndex)
|
|
31
|
+
}
|
|
32
|
+
if err := r.ParseMultipartForm(1 << 20); err != nil {
|
|
33
|
+
t.Fatalf("ParseMultipartForm(): %v", err)
|
|
34
|
+
}
|
|
35
|
+
files := r.MultipartForm.File["file"]
|
|
36
|
+
if len(files) != 1 {
|
|
37
|
+
t.Fatalf("file parts = %d, want 1", len(files))
|
|
38
|
+
}
|
|
39
|
+
uploadIndex++
|
|
40
|
+
_, _ = w.Write([]byte(`{"ret":"0","errmsg":"","data":{"pippit_asset_id":"` + assetIDs[uploadIndex-1] + `"}}`))
|
|
41
|
+
case "/api/biz/v1/skill/submit_run":
|
|
42
|
+
if uploadIndex != len(assetIDs) {
|
|
43
|
+
t.Fatalf("submit called after %d uploads, want %d", uploadIndex, len(assetIDs))
|
|
44
|
+
}
|
|
45
|
+
data, err := io.ReadAll(r.Body)
|
|
46
|
+
if err != nil {
|
|
47
|
+
t.Fatalf("read body: %v", err)
|
|
48
|
+
}
|
|
49
|
+
var body map[string]any
|
|
50
|
+
if err := sonic.Unmarshal(data, &body); err != nil {
|
|
51
|
+
t.Fatalf("decode body: %v", err)
|
|
52
|
+
}
|
|
53
|
+
if body["agent_name"] != "pippit_video_part_agent" {
|
|
54
|
+
t.Fatalf("agent_name = %v, want video part agent", body["agent_name"])
|
|
55
|
+
}
|
|
56
|
+
if body["message"] != "做个小猫视频" {
|
|
57
|
+
t.Fatalf("message = %v, want submitted prompt", body["message"])
|
|
58
|
+
}
|
|
59
|
+
param, ok := body["video_part_tool_param"].(map[string]any)
|
|
60
|
+
if !ok {
|
|
61
|
+
t.Fatalf("video_part_tool_param = %#v, want object", body["video_part_tool_param"])
|
|
62
|
+
}
|
|
63
|
+
if param["prompt"] != "做个小猫视频" {
|
|
64
|
+
t.Fatalf("prompt = %v, want submitted prompt", param["prompt"])
|
|
65
|
+
}
|
|
66
|
+
if param["duration_sec"] != float64(5) {
|
|
67
|
+
t.Fatalf("duration_sec = %v, want 5", param["duration_sec"])
|
|
68
|
+
}
|
|
69
|
+
if param["ratio"] != "9:16" || param["model"] != "seedance2.0_vision" || param["resolution"] != "720p" {
|
|
70
|
+
t.Fatalf("param = %#v, want ratio/model/resolution", param)
|
|
71
|
+
}
|
|
72
|
+
assertAssetRefs(t, param["images"], []string{"image_asset_1", "image_asset_2"})
|
|
73
|
+
assertAssetRefs(t, param["videos"], []string{"video_asset_1", "video_asset_2"})
|
|
74
|
+
_, _ = w.Write([]byte(`{"ret":"0","errmsg":"","data":{"run":{"thread_id":"thread_123","run_id":"run_456"},"web_thread_link":"https://xyq.example/thread_123"}}`))
|
|
75
|
+
default:
|
|
76
|
+
t.Fatalf("unexpected path %s", r.URL.Path)
|
|
77
|
+
}
|
|
78
|
+
}))
|
|
79
|
+
defer server.Close()
|
|
80
|
+
|
|
81
|
+
cwd := chdirTemp(t)
|
|
82
|
+
image1 := filepath.Join(cwd, "cat1.jpg")
|
|
83
|
+
image2 := filepath.Join(cwd, "cat2.jpg")
|
|
84
|
+
video1 := filepath.Join(cwd, "video1.mp4")
|
|
85
|
+
video2 := filepath.Join(cwd, "video2.mp4")
|
|
86
|
+
for _, path := range []string{image1, image2, video1, video2} {
|
|
87
|
+
if err := os.WriteFile(path, []byte("media-data"), 0o644); err != nil {
|
|
88
|
+
t.Fatalf("WriteFile(%s): %v", path, err)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
var stdout, stderr bytes.Buffer
|
|
93
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
94
|
+
root.SetArgs([]string{
|
|
95
|
+
"generate-video",
|
|
96
|
+
"--prompt", "做个小猫视频",
|
|
97
|
+
"--image", image1,
|
|
98
|
+
"--image", image2,
|
|
99
|
+
"--video", video1,
|
|
100
|
+
"--video", video2,
|
|
101
|
+
"--duration", "5",
|
|
102
|
+
"--ratio", "9:16",
|
|
103
|
+
"--model", "seedance2.0_vision",
|
|
104
|
+
"--resolution", "720p",
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
if err := root.Execute(); err != nil {
|
|
108
|
+
t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
got := decodeJSON(t, stdout.Bytes())
|
|
112
|
+
if got["thread_id"] != "thread_123" || got["run_id"] != "run_456" {
|
|
113
|
+
t.Fatalf("output = %#v, want thread and run IDs", got)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func TestGenerateVideoSkipsSemanticValidation(t *testing.T) {
|
|
118
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
119
|
+
switch r.URL.Path {
|
|
120
|
+
case "/api/biz/v1/skill/submit_run":
|
|
121
|
+
_, _ = w.Write([]byte(`{"ret":"0","errmsg":"","data":{"run":{"thread_id":"thread_123","run_id":"run_456"}}}`))
|
|
122
|
+
default:
|
|
123
|
+
t.Fatalf("unexpected path %s", r.URL.Path)
|
|
124
|
+
}
|
|
125
|
+
}))
|
|
126
|
+
defer server.Close()
|
|
127
|
+
|
|
128
|
+
var stdout, stderr bytes.Buffer
|
|
129
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
130
|
+
root.SetArgs([]string{
|
|
131
|
+
"generate-video",
|
|
132
|
+
"--prompt", "x",
|
|
133
|
+
"--duration", "1",
|
|
134
|
+
"--ratio", "1:1",
|
|
135
|
+
"--model", "bad_model",
|
|
136
|
+
"--resolution", "bad_resolution",
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
if err := root.Execute(); err != nil {
|
|
140
|
+
t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
func TestGenerateVideoRequiresPrompt(t *testing.T) {
|
|
145
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
146
|
+
t.Fatal("server should not receive request without prompt")
|
|
147
|
+
}))
|
|
148
|
+
defer server.Close()
|
|
149
|
+
|
|
150
|
+
var stdout, stderr bytes.Buffer
|
|
151
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
152
|
+
root.SetArgs([]string{
|
|
153
|
+
"generate-video",
|
|
154
|
+
"--duration", "1",
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
err := root.Execute()
|
|
158
|
+
if err == nil {
|
|
159
|
+
t.Fatal("Execute() error = nil, want prompt validation")
|
|
160
|
+
}
|
|
161
|
+
if !strings.Contains(err.Error(), "缺少必填参数 --prompt") {
|
|
162
|
+
t.Fatalf("error = %q, want prompt validation", err)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
func TestGenerateVideoRejectsTooManyImages(t *testing.T) {
|
|
167
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
168
|
+
t.Fatal("server should not receive request when image count is invalid")
|
|
169
|
+
}))
|
|
170
|
+
defer server.Close()
|
|
171
|
+
|
|
172
|
+
var stdout, stderr bytes.Buffer
|
|
173
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
174
|
+
args := []string{"generate-video", "--prompt", "x"}
|
|
175
|
+
for _, path := range mediaPaths("image", ".jpg", 10) {
|
|
176
|
+
args = append(args, "--image", path)
|
|
177
|
+
}
|
|
178
|
+
root.SetArgs(args)
|
|
179
|
+
|
|
180
|
+
err := root.Execute()
|
|
181
|
+
if err == nil {
|
|
182
|
+
t.Fatal("Execute() error = nil, want image count validation")
|
|
183
|
+
}
|
|
184
|
+
if !strings.Contains(err.Error(), "参考图片最多支持 9 个,当前传入 10 个") {
|
|
185
|
+
t.Fatalf("error = %q, want image count validation", err)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
func TestGenerateVideoRejectsTooManyVideos(t *testing.T) {
|
|
190
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
191
|
+
t.Fatal("server should not receive request when video count is invalid")
|
|
192
|
+
}))
|
|
193
|
+
defer server.Close()
|
|
194
|
+
|
|
195
|
+
var stdout, stderr bytes.Buffer
|
|
196
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
197
|
+
args := []string{"generate-video", "--prompt", "x"}
|
|
198
|
+
for _, path := range mediaPaths("video", ".mp4", 4) {
|
|
199
|
+
args = append(args, "--video", path)
|
|
200
|
+
}
|
|
201
|
+
root.SetArgs(args)
|
|
202
|
+
|
|
203
|
+
err := root.Execute()
|
|
204
|
+
if err == nil {
|
|
205
|
+
t.Fatal("Execute() error = nil, want video count validation")
|
|
206
|
+
}
|
|
207
|
+
if !strings.Contains(err.Error(), "参考视频最多支持 3 个,当前传入 4 个") {
|
|
208
|
+
t.Fatalf("error = %q, want video count validation", err)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
func TestGenerateVideoSubmitRunErrorIncludesLogID(t *testing.T) {
|
|
213
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
214
|
+
switch r.URL.Path {
|
|
215
|
+
case "/api/biz/v1/skill/submit_run":
|
|
216
|
+
_, _ = w.Write([]byte(`{"ret":"16008","errmsg":"提交Run任务失败","log_id":"log_123"}`))
|
|
217
|
+
default:
|
|
218
|
+
t.Fatalf("unexpected path %s", r.URL.Path)
|
|
219
|
+
}
|
|
220
|
+
}))
|
|
221
|
+
defer server.Close()
|
|
222
|
+
|
|
223
|
+
var stdout, stderr bytes.Buffer
|
|
224
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
225
|
+
root.SetArgs([]string{
|
|
226
|
+
"generate-video",
|
|
227
|
+
"--prompt", "x",
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
err := root.Execute()
|
|
231
|
+
if err == nil {
|
|
232
|
+
t.Fatal("Execute() error = nil, want submit_run error")
|
|
233
|
+
}
|
|
234
|
+
if !strings.Contains(err.Error(), "log_id=log_123") {
|
|
235
|
+
t.Fatalf("error = %q, want log_id", err)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
func TestGenerateVideoErrorLogIncludesLogID(t *testing.T) {
|
|
240
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
241
|
+
switch r.URL.Path {
|
|
242
|
+
case "/api/biz/v1/skill/submit_run":
|
|
243
|
+
_, _ = w.Write([]byte(`{"ret":"16008","errmsg":"提交Run任务失败","log_id":"log_123"}`))
|
|
244
|
+
default:
|
|
245
|
+
t.Fatalf("unexpected path %s", r.URL.Path)
|
|
246
|
+
}
|
|
247
|
+
}))
|
|
248
|
+
defer server.Close()
|
|
249
|
+
|
|
250
|
+
clearDailyErrorLog(t)
|
|
251
|
+
var stdout, stderr bytes.Buffer
|
|
252
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
253
|
+
root.SetArgs([]string{
|
|
254
|
+
"generate-video",
|
|
255
|
+
"--prompt", "x",
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
err := root.Execute()
|
|
259
|
+
if err == nil {
|
|
260
|
+
t.Fatal("Execute() error = nil, want submit_run error")
|
|
261
|
+
}
|
|
262
|
+
entries := readDailyErrorLog(t)
|
|
263
|
+
if len(entries) != 1 {
|
|
264
|
+
t.Fatalf("log entries = %d, want 1: %#v", len(entries), entries)
|
|
265
|
+
}
|
|
266
|
+
fields, ok := entries[0]["fields"].(map[string]any)
|
|
267
|
+
if !ok {
|
|
268
|
+
t.Fatalf("fields = %#v, want object", entries[0]["fields"])
|
|
269
|
+
}
|
|
270
|
+
if fields["log_id"] != "log_123" {
|
|
271
|
+
t.Fatalf("log_id = %v, want log_123", fields["log_id"])
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
func TestQueryResultDownloadsCompletedVideo(t *testing.T) {
|
|
276
|
+
var requestedDownload bool
|
|
277
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
278
|
+
if r.Header.Get("Authorization") != "Bearer test-token" {
|
|
279
|
+
t.Fatalf("Authorization = %q, want test bearer token", r.Header.Get("Authorization"))
|
|
280
|
+
}
|
|
281
|
+
switch r.URL.Path {
|
|
282
|
+
case "/api/biz/v1/skill/get_thread":
|
|
283
|
+
if r.Method != http.MethodPost {
|
|
284
|
+
t.Fatalf("get_thread method = %s, want POST", r.Method)
|
|
285
|
+
}
|
|
286
|
+
data, err := io.ReadAll(r.Body)
|
|
287
|
+
if err != nil {
|
|
288
|
+
t.Fatalf("read body: %v", err)
|
|
289
|
+
}
|
|
290
|
+
var body map[string]any
|
|
291
|
+
if err := sonic.Unmarshal(data, &body); err != nil {
|
|
292
|
+
t.Fatalf("decode body: %v", err)
|
|
293
|
+
}
|
|
294
|
+
if body["thread_id"] != "thread_123" {
|
|
295
|
+
t.Fatalf("thread_id = %v, want thread_123", body["thread_id"])
|
|
296
|
+
}
|
|
297
|
+
if body["run_id"] != "run_456" {
|
|
298
|
+
t.Fatalf("run_id = %v, want run_456", body["run_id"])
|
|
299
|
+
}
|
|
300
|
+
if _, ok := body["version"]; ok {
|
|
301
|
+
t.Fatalf("version = %v, want omitted", body["version"])
|
|
302
|
+
}
|
|
303
|
+
_, _ = w.Write([]byte(`{"ret":"0","errmsg":"","data":{"thread":{"thread_id":"thread_123","run_list":[{"run_id":"run_456","state":3,"entry_list":[{"artifact":{"content":[{"sub_type":"biz/x_data_prompt_text","data":"做个小猫视频"},{"sub_type":"biz/x_data_video","data":"{\"video\":{\"download_url\":\"` + serverURL(r) + `/video.mp4\",\"title\":\"cat_video\",\"vid\":\"cat_vid\"}}"}]}}]}]}}}`))
|
|
304
|
+
case "/video.mp4":
|
|
305
|
+
requestedDownload = true
|
|
306
|
+
if r.Method != http.MethodGet {
|
|
307
|
+
t.Fatalf("download method = %s, want GET", r.Method)
|
|
308
|
+
}
|
|
309
|
+
_, _ = w.Write([]byte("video-data"))
|
|
310
|
+
default:
|
|
311
|
+
t.Fatalf("unexpected path %s", r.URL.Path)
|
|
312
|
+
}
|
|
313
|
+
}))
|
|
314
|
+
defer server.Close()
|
|
315
|
+
|
|
316
|
+
downloadDir := filepath.Join(t.TempDir(), "downloads")
|
|
317
|
+
var stdout, stderr bytes.Buffer
|
|
318
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
319
|
+
root.SetArgs([]string{
|
|
320
|
+
"query-result",
|
|
321
|
+
"--thread-id", "thread_123",
|
|
322
|
+
"--run-id", "run_456",
|
|
323
|
+
"--download-dir", downloadDir,
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
if err := root.Execute(); err != nil {
|
|
327
|
+
t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
outputPath := filepath.Join(downloadDir, "cat_vid.mp4")
|
|
331
|
+
downloadURL := server.URL + "/video.mp4"
|
|
332
|
+
if !requestedDownload {
|
|
333
|
+
t.Fatal("download endpoint was not requested")
|
|
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)
|
|
339
|
+
}
|
|
340
|
+
assertFileContent(t, outputPath, "video-data")
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
func TestQueryResultIgnoresVideoDataWithoutVideoSubType(t *testing.T) {
|
|
344
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
345
|
+
switch r.URL.Path {
|
|
346
|
+
case "/api/biz/v1/skill/get_thread":
|
|
347
|
+
_, _ = w.Write([]byte(`{"ret":"0","errmsg":"","data":{"thread":{"thread_id":"thread_123","run_list":[{"run_id":"run_456","state":3,"entry_list":[{"artifact":{"content":[{"data":"{\"video\":{\"download_url\":\"` + serverURL(r) + `/video.mp4\",\"title\":\"cat_video\"}}"}]}}]}]}}}`))
|
|
348
|
+
default:
|
|
349
|
+
t.Fatalf("unexpected path %s", r.URL.Path)
|
|
350
|
+
}
|
|
351
|
+
}))
|
|
352
|
+
defer server.Close()
|
|
353
|
+
|
|
354
|
+
var stdout, stderr bytes.Buffer
|
|
355
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
356
|
+
root.SetArgs([]string{
|
|
357
|
+
"query-result",
|
|
358
|
+
"--thread-id", "thread_123",
|
|
359
|
+
"--run-id", "run_456",
|
|
360
|
+
"--download-dir", t.TempDir(),
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
err := root.Execute()
|
|
364
|
+
if err == nil {
|
|
365
|
+
t.Fatal("Execute() error = nil, want no downloadable video error")
|
|
366
|
+
}
|
|
367
|
+
if !strings.Contains(err.Error(), "下载失败:未找到可下载的视频产物") {
|
|
368
|
+
t.Fatalf("error = %q, want no downloadable video error", err)
|
|
369
|
+
}
|
|
370
|
+
if stdout.Len() != 0 {
|
|
371
|
+
t.Fatalf("stdout = %q, want empty", stdout.String())
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
func TestQueryResultErrorLogIncludesLogID(t *testing.T) {
|
|
376
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
377
|
+
switch r.URL.Path {
|
|
378
|
+
case "/api/biz/v1/skill/get_thread":
|
|
379
|
+
_, _ = w.Write([]byte(`{"ret":"5","errmsg":"创作失败","log_id":"log_456"}`))
|
|
380
|
+
default:
|
|
381
|
+
t.Fatalf("unexpected path %s", r.URL.Path)
|
|
382
|
+
}
|
|
383
|
+
}))
|
|
384
|
+
defer server.Close()
|
|
385
|
+
|
|
386
|
+
clearDailyErrorLog(t)
|
|
387
|
+
var stdout, stderr bytes.Buffer
|
|
388
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
389
|
+
root.SetArgs([]string{
|
|
390
|
+
"query-result",
|
|
391
|
+
"--thread-id", "thread_123",
|
|
392
|
+
"--run-id", "run_456",
|
|
393
|
+
"--download-dir", t.TempDir(),
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
err := root.Execute()
|
|
397
|
+
if err == nil {
|
|
398
|
+
t.Fatal("Execute() error = nil, want get_thread error")
|
|
399
|
+
}
|
|
400
|
+
entries := readDailyErrorLog(t)
|
|
401
|
+
if len(entries) != 1 {
|
|
402
|
+
t.Fatalf("log entries = %d, want 1: %#v", len(entries), entries)
|
|
403
|
+
}
|
|
404
|
+
fields, ok := entries[0]["fields"].(map[string]any)
|
|
405
|
+
if !ok {
|
|
406
|
+
t.Fatalf("fields = %#v, want object", entries[0]["fields"])
|
|
407
|
+
}
|
|
408
|
+
if fields["log_id"] != "log_456" {
|
|
409
|
+
t.Fatalf("log_id = %v, want log_456", fields["log_id"])
|
|
410
|
+
}
|
|
411
|
+
if fields["thread_id"] != "thread_123" || fields["run_id"] != "run_456" {
|
|
412
|
+
t.Fatalf("fields = %#v, want thread/run ids", fields)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
func TestQueryResultPendingDoesNotDownload(t *testing.T) {
|
|
417
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
418
|
+
switch r.URL.Path {
|
|
419
|
+
case "/api/biz/v1/skill/get_thread":
|
|
420
|
+
_, _ = w.Write([]byte(`{"ret":"0","errmsg":"","data":{"thread":{"thread_id":"thread_123","run_list":[{"run_id":"run_456","state":1}]}}}`))
|
|
421
|
+
default:
|
|
422
|
+
t.Fatalf("unexpected path %s", r.URL.Path)
|
|
423
|
+
}
|
|
424
|
+
}))
|
|
425
|
+
defer server.Close()
|
|
426
|
+
|
|
427
|
+
var stdout, stderr bytes.Buffer
|
|
428
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
429
|
+
root.SetArgs([]string{
|
|
430
|
+
"query-result",
|
|
431
|
+
"--thread-id", "thread_123",
|
|
432
|
+
"--run-id", "run_456",
|
|
433
|
+
"--download-dir", t.TempDir(),
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
if err := root.Execute(); err != nil {
|
|
437
|
+
t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
|
|
438
|
+
}
|
|
439
|
+
if got := stdout.String(); !strings.Contains(got, "Run 尚未完成,当前状态:1") {
|
|
440
|
+
t.Fatalf("stdout = %q, want pending state", got)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
func serverURL(r *http.Request) string {
|
|
445
|
+
return "http://" + r.Host
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
func assertAssetRefs(t *testing.T, got any, want []string) {
|
|
449
|
+
t.Helper()
|
|
450
|
+
items, ok := got.([]any)
|
|
451
|
+
if !ok || len(items) != len(want) {
|
|
452
|
+
t.Fatalf("asset refs = %#v, want %v", got, want)
|
|
453
|
+
}
|
|
454
|
+
for i, item := range items {
|
|
455
|
+
ref, ok := item.(map[string]any)
|
|
456
|
+
if !ok || ref["pippit_asset_id"] != want[i] {
|
|
457
|
+
t.Fatalf("asset refs[%d] = %#v, want %s", i, item, want[i])
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
func mediaPaths(prefix string, ext string, count int) []string {
|
|
463
|
+
paths := make([]string, 0, count)
|
|
464
|
+
for i := 0; i < count; i++ {
|
|
465
|
+
paths = append(paths, prefix+string(rune('a'+i))+ext)
|
|
466
|
+
}
|
|
467
|
+
return paths
|
|
468
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"io"
|
|
6
|
+
"strings"
|
|
7
|
+
|
|
8
|
+
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
9
|
+
"github.com/spf13/cobra"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
func newGetThreadCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
|
|
13
|
+
var opts common.GetThreadOptions
|
|
14
|
+
|
|
15
|
+
cmd := &cobra.Command{
|
|
16
|
+
Use: "get-thread",
|
|
17
|
+
Short: "Get a thread detail",
|
|
18
|
+
Args: cobra.NoArgs,
|
|
19
|
+
RunE: withErrorLog("get-thread", func() map[string]string {
|
|
20
|
+
return map[string]string{
|
|
21
|
+
"thread_id": opts.ThreadID,
|
|
22
|
+
"run_id": opts.RunID,
|
|
23
|
+
}
|
|
24
|
+
}, func(cmd *cobra.Command, _ []string) error {
|
|
25
|
+
opts.ThreadID = strings.TrimSpace(opts.ThreadID)
|
|
26
|
+
if opts.ThreadID == "" {
|
|
27
|
+
return fmt.Errorf("--thread-id is required")
|
|
28
|
+
}
|
|
29
|
+
opts.RunID = strings.TrimSpace(opts.RunID)
|
|
30
|
+
opts.Version = common.GetThreadVersionV2
|
|
31
|
+
|
|
32
|
+
result, err := common.GetThread(cmd.Context(), &opts, runner)
|
|
33
|
+
if err != nil {
|
|
34
|
+
return err
|
|
35
|
+
}
|
|
36
|
+
_, err = fmt.Fprintln(stdout, result.ReadableText)
|
|
37
|
+
return err
|
|
38
|
+
}),
|
|
39
|
+
}
|
|
40
|
+
cmd.SetOut(stdout)
|
|
41
|
+
cmd.SetErr(stderr)
|
|
42
|
+
cmd.Flags().StringVar(&opts.ThreadID, "thread-id", "", "thread ID to fetch")
|
|
43
|
+
cmd.Flags().StringVar(&opts.RunID, "run-id", "", "run ID to fetch")
|
|
44
|
+
return cmd
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func withErrorLog(command string, fields func() map[string]string, run func(*cobra.Command, []string) error) func(*cobra.Command, []string) error {
|
|
48
|
+
return func(cmd *cobra.Command, args []string) error {
|
|
49
|
+
err := run(cmd, args)
|
|
50
|
+
if err != nil {
|
|
51
|
+
logFields := map[string]string(nil)
|
|
52
|
+
if fields != nil {
|
|
53
|
+
logFields = fields()
|
|
54
|
+
}
|
|
55
|
+
_ = common.AppendDailyErrorLog(command, err, logFields)
|
|
56
|
+
}
|
|
57
|
+
return err
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"io"
|
|
6
|
+
"strconv"
|
|
7
|
+
"strings"
|
|
8
|
+
|
|
9
|
+
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
10
|
+
"github.com/spf13/cobra"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func newListThreadFileCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
|
|
14
|
+
var opts common.ListThreadFileOptions
|
|
15
|
+
|
|
16
|
+
cmd := &cobra.Command{
|
|
17
|
+
Use: "list-thread-file",
|
|
18
|
+
Short: "List files in a thread",
|
|
19
|
+
Args: cobra.NoArgs,
|
|
20
|
+
RunE: withErrorLog("list-thread-file", func() map[string]string {
|
|
21
|
+
return map[string]string{
|
|
22
|
+
"thread_id": opts.ThreadID,
|
|
23
|
+
"page_num": strconv.Itoa(opts.PageNum),
|
|
24
|
+
"page_size": strconv.Itoa(opts.PageSize),
|
|
25
|
+
}
|
|
26
|
+
}, func(cmd *cobra.Command, _ []string) error {
|
|
27
|
+
opts.ThreadID = strings.TrimSpace(opts.ThreadID)
|
|
28
|
+
if opts.ThreadID == "" {
|
|
29
|
+
return fmt.Errorf("--thread-id is required")
|
|
30
|
+
}
|
|
31
|
+
if opts.PageSize <= 0 || opts.PageSize > common.MaxListThreadFilePageSize {
|
|
32
|
+
return fmt.Errorf("--page-size must be between 1 and %d", common.MaxListThreadFilePageSize)
|
|
33
|
+
}
|
|
34
|
+
if opts.PageNum <= 0 {
|
|
35
|
+
return fmt.Errorf("--page-num must be greater than 0")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
result, err := common.ListThreadFile(cmd.Context(), &opts, runner)
|
|
39
|
+
if err != nil {
|
|
40
|
+
return err
|
|
41
|
+
}
|
|
42
|
+
return common.WriteJSON(stdout, result)
|
|
43
|
+
}),
|
|
44
|
+
}
|
|
45
|
+
cmd.SetOut(stdout)
|
|
46
|
+
cmd.SetErr(stderr)
|
|
47
|
+
cmd.Flags().StringVar(&opts.ThreadID, "thread-id", "", "thread ID to list files for")
|
|
48
|
+
cmd.Flags().IntVar(&opts.PageNum, "page-num", 1, "page number (1-based)")
|
|
49
|
+
cmd.Flags().IntVar(&opts.PageSize, "page-size", common.MaxListThreadFilePageSize, fmt.Sprintf("number of files per page (between 1 and %d)", common.MaxListThreadFilePageSize))
|
|
50
|
+
return cmd
|
|
51
|
+
}
|
package/cmd/root.go
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
package cmd
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"fmt"
|
|
4
5
|
"io"
|
|
5
6
|
"os"
|
|
7
|
+
"strings"
|
|
6
8
|
|
|
7
9
|
// authcmd "github.com/Pippit-dev/pippit-cli/cmd/auth"
|
|
10
|
+
"github.com/Pippit-dev/pippit-cli/cmd/generate_video"
|
|
8
11
|
"github.com/Pippit-dev/pippit-cli/cmd/short_drama"
|
|
9
12
|
updatecmd "github.com/Pippit-dev/pippit-cli/cmd/update"
|
|
10
13
|
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
@@ -29,7 +32,7 @@ func newRootCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Comm
|
|
|
29
32
|
root := &cobra.Command{
|
|
30
33
|
Use: "pippit-tool-cli",
|
|
31
34
|
Short: "Pippit CLI",
|
|
32
|
-
Long: "Pippit CLI submits short-drama workflows, downloads generated assets, and updates the installed CLI package.",
|
|
35
|
+
Long: "Pippit CLI generates videos, submits short-drama workflows, downloads generated assets, and updates the installed CLI package.",
|
|
33
36
|
Version: version.Current(),
|
|
34
37
|
SilenceUsage: true,
|
|
35
38
|
SilenceErrors: true,
|
|
@@ -39,7 +42,33 @@ func newRootCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Comm
|
|
|
39
42
|
root.SetOut(stdout)
|
|
40
43
|
root.SetErr(stderr)
|
|
41
44
|
// root.AddCommand(authcmd.NewCommand(stdout, stderr, runner)) // temporarily disabled; auth is via access key injection
|
|
45
|
+
root.AddCommand(newDownloadResultCommand(stdout, stderr, runner))
|
|
46
|
+
root.AddCommand(newGetThreadCommand(stdout, stderr, runner))
|
|
47
|
+
root.AddCommand(newListThreadFileCommand(stdout, stderr, runner))
|
|
48
|
+
root.AddCommand(generate_video.NewCommand(stdout, stderr, runner))
|
|
49
|
+
root.AddCommand(generate_video.NewQueryResultCommand(stdout, stderr, runner))
|
|
42
50
|
root.AddCommand(short_drama.NewCommand(stdout, stderr, runner))
|
|
43
51
|
root.AddCommand(updatecmd.NewCommand(stdout, stderr))
|
|
52
|
+
localizeFlagErrors(root)
|
|
44
53
|
return root
|
|
45
54
|
}
|
|
55
|
+
|
|
56
|
+
func localizeFlagErrors(cmd *cobra.Command) {
|
|
57
|
+
cmd.SetFlagErrorFunc(func(_ *cobra.Command, err error) error {
|
|
58
|
+
return localizeFlagError(err)
|
|
59
|
+
})
|
|
60
|
+
for _, child := range cmd.Commands() {
|
|
61
|
+
localizeFlagErrors(child)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func localizeFlagError(err error) error {
|
|
66
|
+
msg := err.Error()
|
|
67
|
+
if flag, ok := strings.CutPrefix(msg, "unknown flag: "); ok {
|
|
68
|
+
return fmt.Errorf("未知参数: %s", flag)
|
|
69
|
+
}
|
|
70
|
+
if flag, ok := strings.CutPrefix(msg, "flag needs an argument: "); ok {
|
|
71
|
+
return fmt.Errorf("参数 %s 缺少取值", flag)
|
|
72
|
+
}
|
|
73
|
+
return fmt.Errorf("参数解析失败: %s", msg)
|
|
74
|
+
}
|