@pippit-dev/cli 1.0.51 → 1.0.53
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 +19 -1
- package/checksums.txt +6 -6
- package/cmd/generate_video/query_result.go +5 -1
- package/cmd/generate_video_test.go +138 -93
- package/cmd/query_result_branches_test.go +265 -0
- package/cmd/root.go +1 -0
- package/cmd/space.go +1 -1
- package/cmd/testdata/query-result/recorded-failure.json +6 -0
- package/cmd/watermark.go +38 -0
- package/dist/checksums.txt +6 -6
- package/internal/commandnames/names.go +1 -0
- package/internal/common/get_thread.go +9 -5
- package/internal/common/get_thread_test.go +36 -0
- package/internal/common/watermark.go +54 -0
- package/internal/config/config.go +2 -0
- package/internal/generate_video/query_result.go +115 -59
- package/internal/generate_video/query_result_branches_test.go +63 -0
- package/package.json +1 -1
- package/skills/short-drama/SKILL.md +1 -0
- package/skills/short-drama/commands/watermark.md +15 -0
- package/skills/xyq-nest-skill/SKILL.md +1 -0
- package/skills/xyq-nest-skill/commands/query-result.md +12 -2
- package/skills/xyq-nest-skill/commands/watermark.md +15 -0
- package/skills/xyq-nest-skill/workflows/async-delivery.md +3 -3
package/README.md
CHANGED
|
@@ -77,6 +77,20 @@ pippit-tool-cli get-credit-balance --team-id 0 # 仅本次使用个人
|
|
|
77
77
|
|
|
78
78
|
`space list/use TEAM_ID` 依赖 API 部署 `/api/biz/v1/skill/get_lite_team_list` 的 AK 适配入口,业务团队使用还依赖对应 API 团队权限实现;不直连 commerce Web 接口。宿主 Agent 的选择、任务恢复与不回退规则已写入三个 Skill 的 `Space Selection` 章节。
|
|
79
79
|
|
|
80
|
+
## 账号 AIGC 水印设置
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pippit-tool-cli watermark # 查询当前账号的 AIGC 水印开关
|
|
84
|
+
pippit-tool-cli watermark off # 保存内容时去除 AIGC 水印
|
|
85
|
+
pippit-tool-cli watermark on # 恢复保留 AIGC 水印
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
查询和修改均输出 `{"enabled":true}` 或 `{"enabled":false}`,`true` 表示保留 AIGC 水印。无参数只查询,只有显式 `on` / `off` 才修改;修改接口在服务端回读后返回当前状态。
|
|
89
|
+
|
|
90
|
+
这是当前认证账号的持久设置,与 Web「保存内容去除 AIGC 水印」同步;不依赖默认个人/团队空间,也不会变成团队开关。品牌水印与最终下载权益仍由服务端决定,关闭 AIGC 开关不代表所有视频均无任何水印。修改失败或结果不明时先用无参数命令查询,不自动重复写入。
|
|
91
|
+
|
|
92
|
+
依赖 API 部署 `/api/biz/v1/skill/mget_user_setting` 和 `/api/biz/v1/skill/update_user_setting` 的 AK 接口。前者接收 `{}`,后者接收 `{"enabled":true}` 或 `{"enabled":false}`;接口仅读写 AK 所属账号的 AIGC 设置。
|
|
93
|
+
|
|
80
94
|
## 小云雀营销成片技能
|
|
81
95
|
|
|
82
96
|
入口:[skills/xyq-marketing-skill/SKILL.md](skills/xyq-marketing-skill/SKILL.md)。使用 [官网](https://xyq.jianying.com/cli?tab=api) 已开放的营销 API,支持素材上传、营销视频提交、结果查询与下载、积分查询。安装器会从 `skills/` 自动安装该 Skill,也可以单独安装:
|
|
@@ -452,7 +466,11 @@ pippit-tool-cli query-result \
|
|
|
452
466
|
--download-dir "./output"
|
|
453
467
|
```
|
|
454
468
|
|
|
455
|
-
`query-result`
|
|
469
|
+
`query-result` 查询指定 Run 并输出 JSON,成功后下载视频和图片。`status` 支持 `running`、`succeeded`、`failed`、`canceled`、`input_required`、`interrupted`、`unknown`。`completed` 仅表示终态:成功、失败、取消和人工交互中断结束(state=9)为 true;等待补充输入(state=6)为 `input_required`、false,需要停止自动轮询并等待交互。程序中断/等待工具回调(state=8)仍为 `running`。
|
|
470
|
+
|
|
471
|
+
判断优先级:无论查询接口 `ret` 为多少,只要能解析指定 Run,就使用其 `state`;失败原因优先读取 `fail_reason.message/code`。没有目标 Run 时,仅按此接口已核实的返回识别终态:`ret=5` 且 `errmsg` 为 `创作失败` 或以 `创作失败:` 开头,返回 `failed`、true;`ret=5` 且 `errmsg` 为 `创作已取消`,返回 `canceled`、true。其他查询错误(包括 `ret=5` 的 `获取会话信息失败`、`ret=1/2`、鉴权/团队权限、网络及解析错误)返回 `unknown`、false 并保留错误与可用的 LogID,不推断任务失败。当前接口的 `ret=2` 是参数错误,不代表安审失败。未指定或未知 Run 状态保持 `unknown`。已解析 Run 的非零接口返回也记录到本地错误日志。
|
|
472
|
+
|
|
473
|
+
Run 已成功但产物缺失或下载失败时,保留 `succeeded`、true,在 `error_message` 中报告错误,并保留此前已成功下载的 `videos` / `images`。媒体项包含 `download_url` 和 `output_path`;图片扩展名取自 `metadata.format`,缺省为 `.png`。完整交付仍须检查错误及所有目标文件,不能只看 `completed=true`。
|
|
456
474
|
|
|
457
475
|
## Adapter 接入边界
|
|
458
476
|
|
package/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
08e0f157a05bdcbbb4c7a53eeeee3374d5be4bf86ec31139c5d42328c8cc781e pippit-tool-cli-1.0.53-darwin-amd64.tar.gz
|
|
2
|
+
fa1b79ed862a997d7ec8117895158f1914d2fcaa30792b303fd924d2856a834f pippit-tool-cli-1.0.53-darwin-arm64.tar.gz
|
|
3
|
+
e0d30c4edf201108587da2bd1ce97f9e7a30721e41700c50e7ddb57cc4c2474a pippit-tool-cli-1.0.53-linux-amd64.tar.gz
|
|
4
|
+
b60c817d9b58659838baf9954e5cc110ddc92ed2736e5512047dc2b20a9dfde8 pippit-tool-cli-1.0.53-linux-arm64.tar.gz
|
|
5
|
+
b890470552424b37fbd98d8a0e202d66526f7f575233cfb860599dadbc802be2 pippit-tool-cli-1.0.53-windows-amd64.zip
|
|
6
|
+
cf549f5b6e7d929d7b8b48b0a1a3df467ad921c23b4a93361799d7723b023bde pippit-tool-cli-1.0.53-windows-arm64.zip
|
|
@@ -26,7 +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
|
-
result
|
|
29
|
+
if result == nil {
|
|
30
|
+
result = internalgen.NewQueryResultError(err, opts)
|
|
31
|
+
} else if result.ErrorMessage == "" {
|
|
32
|
+
result.ErrorMessage = err.Error()
|
|
33
|
+
}
|
|
30
34
|
}
|
|
31
35
|
encoder := json.NewEncoder(stdout)
|
|
32
36
|
encoder.SetIndent("", " ")
|
|
@@ -434,98 +434,103 @@ func TestGenerateVideoErrorLogIncludesLogID(t *testing.T) {
|
|
|
434
434
|
}
|
|
435
435
|
|
|
436
436
|
func TestQueryResultDownloadsCompletedVideo(t *testing.T) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
437
|
+
for _, ret := range []string{"0", "1", "2", "5", "1015", "16019", "987654"} {
|
|
438
|
+
t.Run("ret="+ret, func(t *testing.T) {
|
|
439
|
+
var requestedDownload bool
|
|
440
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
441
|
+
if r.Header.Get("Authorization") != "Bearer test-token" {
|
|
442
|
+
t.Fatalf("Authorization = %q, want test bearer token", r.Header.Get("Authorization"))
|
|
443
|
+
}
|
|
444
|
+
switch r.URL.Path {
|
|
445
|
+
case "/api/biz/v1/skill/get_thread":
|
|
446
|
+
if r.Method != http.MethodPost {
|
|
447
|
+
t.Fatalf("get_thread method = %s, want POST", r.Method)
|
|
448
|
+
}
|
|
449
|
+
data, err := io.ReadAll(r.Body)
|
|
450
|
+
if err != nil {
|
|
451
|
+
t.Fatalf("read body: %v", err)
|
|
452
|
+
}
|
|
453
|
+
var body map[string]any
|
|
454
|
+
if err := sonic.Unmarshal(data, &body); err != nil {
|
|
455
|
+
t.Fatalf("decode body: %v", err)
|
|
456
|
+
}
|
|
457
|
+
if body["thread_id"] != "thread_123" {
|
|
458
|
+
t.Fatalf("thread_id = %v, want thread_123", body["thread_id"])
|
|
459
|
+
}
|
|
460
|
+
if body["run_id"] != "run_456" {
|
|
461
|
+
t.Fatalf("run_id = %v, want run_456", body["run_id"])
|
|
462
|
+
}
|
|
463
|
+
if _, ok := body["version"]; ok {
|
|
464
|
+
t.Fatalf("version = %v, want omitted", body["version"])
|
|
465
|
+
}
|
|
466
|
+
_, _ = w.Write([]byte(`{"ret":"` + ret + `","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\"}}"}]}}]}]}}}`))
|
|
467
|
+
case "/video.mp4":
|
|
468
|
+
requestedDownload = true
|
|
469
|
+
if r.Method != http.MethodGet {
|
|
470
|
+
t.Fatalf("download method = %s, want GET", r.Method)
|
|
471
|
+
}
|
|
472
|
+
_, _ = w.Write([]byte("video-data"))
|
|
473
|
+
default:
|
|
474
|
+
t.Fatalf("unexpected path %s", r.URL.Path)
|
|
475
|
+
}
|
|
476
|
+
}))
|
|
477
|
+
defer server.Close()
|
|
478
|
+
|
|
479
|
+
downloadDir := filepath.Join(t.TempDir(), "downloads")
|
|
480
|
+
var stdout, stderr bytes.Buffer
|
|
481
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
482
|
+
root.SetArgs([]string{
|
|
483
|
+
"query-result",
|
|
484
|
+
"--thread-id", "thread_123",
|
|
485
|
+
"--run-id", "run_456",
|
|
486
|
+
"--download-dir", downloadDir,
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
if err := root.Execute(); err != nil {
|
|
490
|
+
t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
|
|
446
491
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
492
|
+
|
|
493
|
+
outputPath := filepath.Join(downloadDir, "cat_vid.mp4")
|
|
494
|
+
downloadURL := server.URL + "/video.mp4"
|
|
495
|
+
if !requestedDownload {
|
|
496
|
+
t.Fatal("download endpoint was not requested")
|
|
450
497
|
}
|
|
451
|
-
|
|
452
|
-
if
|
|
453
|
-
t.Fatalf("
|
|
498
|
+
got := decodeJSON(t, stdout.Bytes())
|
|
499
|
+
if got["completed"] != true {
|
|
500
|
+
t.Fatalf("completed = %v, want true", got["completed"])
|
|
454
501
|
}
|
|
455
|
-
if
|
|
456
|
-
t.Fatalf("
|
|
502
|
+
if got["status"] != "succeeded" {
|
|
503
|
+
t.Fatalf("status = %v, want succeeded", got["status"])
|
|
457
504
|
}
|
|
458
|
-
if
|
|
459
|
-
t.Fatalf("
|
|
505
|
+
if got["thread_id"] != "thread_123" || got["run_id"] != "run_456" {
|
|
506
|
+
t.Fatalf("ids = (%v, %v), want thread/run ids", got["thread_id"], got["run_id"])
|
|
460
507
|
}
|
|
461
|
-
if _, ok :=
|
|
462
|
-
t.Fatalf("
|
|
508
|
+
if _, ok := got["state"]; ok {
|
|
509
|
+
t.Fatalf("state should not be returned: %#v", got)
|
|
463
510
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
requestedDownload = true
|
|
467
|
-
if r.Method != http.MethodGet {
|
|
468
|
-
t.Fatalf("download method = %s, want GET", r.Method)
|
|
511
|
+
if got["error_message"] != "" {
|
|
512
|
+
t.Fatalf("error_message = %v, want empty", got["error_message"])
|
|
469
513
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if err := root.Execute(); err != nil {
|
|
488
|
-
t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
|
|
489
|
-
}
|
|
514
|
+
videos, ok := got["videos"].([]any)
|
|
515
|
+
if !ok || len(videos) != 1 {
|
|
516
|
+
t.Fatalf("videos = %#v, want one video", got["videos"])
|
|
517
|
+
}
|
|
518
|
+
video, ok := videos[0].(map[string]any)
|
|
519
|
+
if !ok {
|
|
520
|
+
t.Fatalf("video = %#v, want object", videos[0])
|
|
521
|
+
}
|
|
522
|
+
if video["download_url"] != downloadURL || video["output_path"] != outputPath {
|
|
523
|
+
t.Fatalf("video = %#v, want download_url/output_path", video)
|
|
524
|
+
}
|
|
525
|
+
for _, unwanted := range []string{"vid", "asset_id", "title", "draft", "draft_task_id"} {
|
|
526
|
+
if _, ok := video[unwanted]; ok {
|
|
527
|
+
t.Fatalf("video = %#v, should not contain %s", video, unwanted)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
assertFileContent(t, outputPath, "video-data")
|
|
490
531
|
|
|
491
|
-
|
|
492
|
-
downloadURL := server.URL + "/video.mp4"
|
|
493
|
-
if !requestedDownload {
|
|
494
|
-
t.Fatal("download endpoint was not requested")
|
|
495
|
-
}
|
|
496
|
-
got := decodeJSON(t, stdout.Bytes())
|
|
497
|
-
if got["completed"] != true {
|
|
498
|
-
t.Fatalf("completed = %v, want true", got["completed"])
|
|
499
|
-
}
|
|
500
|
-
if got["status"] != "succeeded" {
|
|
501
|
-
t.Fatalf("status = %v, want succeeded", got["status"])
|
|
502
|
-
}
|
|
503
|
-
if got["thread_id"] != "thread_123" || got["run_id"] != "run_456" {
|
|
504
|
-
t.Fatalf("ids = (%v, %v), want thread/run ids", got["thread_id"], got["run_id"])
|
|
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) != 1 {
|
|
514
|
-
t.Fatalf("videos = %#v, want one video", got["videos"])
|
|
515
|
-
}
|
|
516
|
-
video, ok := videos[0].(map[string]any)
|
|
517
|
-
if !ok {
|
|
518
|
-
t.Fatalf("video = %#v, want object", videos[0])
|
|
519
|
-
}
|
|
520
|
-
if video["download_url"] != downloadURL || video["output_path"] != outputPath {
|
|
521
|
-
t.Fatalf("video = %#v, want download_url/output_path", video)
|
|
522
|
-
}
|
|
523
|
-
for _, unwanted := range []string{"vid", "asset_id", "title", "draft", "draft_task_id"} {
|
|
524
|
-
if _, ok := video[unwanted]; ok {
|
|
525
|
-
t.Fatalf("video = %#v, should not contain %s", video, unwanted)
|
|
526
|
-
}
|
|
532
|
+
})
|
|
527
533
|
}
|
|
528
|
-
assertFileContent(t, outputPath, "video-data")
|
|
529
534
|
}
|
|
530
535
|
|
|
531
536
|
func TestQueryResultPreservesDraftMetadata(t *testing.T) {
|
|
@@ -601,11 +606,11 @@ func TestQueryResultIgnoresVideoDataWithoutVideoSubType(t *testing.T) {
|
|
|
601
606
|
t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
|
|
602
607
|
}
|
|
603
608
|
got := decodeJSON(t, stdout.Bytes())
|
|
604
|
-
if got["completed"] !=
|
|
605
|
-
t.Fatalf("completed = %v, want
|
|
609
|
+
if got["completed"] != true {
|
|
610
|
+
t.Fatalf("completed = %v, want true for a succeeded Run", got["completed"])
|
|
606
611
|
}
|
|
607
|
-
if got["status"] != "
|
|
608
|
-
t.Fatalf("status = %v, want
|
|
612
|
+
if got["status"] != "succeeded" {
|
|
613
|
+
t.Fatalf("status = %v, want succeeded despite missing downloadable artifacts", got["status"])
|
|
609
614
|
}
|
|
610
615
|
if got["thread_id"] != "thread_123" || got["run_id"] != "run_456" {
|
|
611
616
|
t.Fatalf("ids = (%v, %v), want thread/run ids", got["thread_id"], got["run_id"])
|
|
@@ -674,14 +679,14 @@ func TestQueryResultGetThreadBusinessErrorReturnsErrorMessage(t *testing.T) {
|
|
|
674
679
|
t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
|
|
675
680
|
}
|
|
676
681
|
got := decodeJSON(t, stdout.Bytes())
|
|
677
|
-
if got["completed"] !=
|
|
678
|
-
t.Fatalf("completed = %v, want
|
|
682
|
+
if got["completed"] != true {
|
|
683
|
+
t.Fatalf("completed = %v, want true", got["completed"])
|
|
679
684
|
}
|
|
680
685
|
if got["thread_id"] != "thread_123" || got["run_id"] != "run_456" {
|
|
681
686
|
t.Fatalf("ids = (%v, %v), want thread/run ids", got["thread_id"], got["run_id"])
|
|
682
687
|
}
|
|
683
|
-
if got["status"] != "
|
|
684
|
-
t.Fatalf("status = %v, want
|
|
688
|
+
if got["status"] != "failed" {
|
|
689
|
+
t.Fatalf("status = %v, want failed", got["status"])
|
|
685
690
|
}
|
|
686
691
|
if got["error_message"] != "创作失败:暂时无法生成 log_id=log_456" {
|
|
687
692
|
t.Fatalf("error_message = %v, want get_thread business error", got["error_message"])
|
|
@@ -692,6 +697,46 @@ func TestQueryResultGetThreadBusinessErrorReturnsErrorMessage(t *testing.T) {
|
|
|
692
697
|
}
|
|
693
698
|
}
|
|
694
699
|
|
|
700
|
+
func TestQueryResultUsesRunDespiteBusinessError(t *testing.T) {
|
|
701
|
+
for _, tt := range []struct {
|
|
702
|
+
name, data, status, message string
|
|
703
|
+
completed bool
|
|
704
|
+
}{
|
|
705
|
+
{"structured failure", `{"thread":{"thread_id":"thread_123","run_list":[{"run_id":"run_456","state":4,"error_message":"Run 失败"}]}}`, "failed", "Run 失败", true},
|
|
706
|
+
{"envelope fallback", `{"thread":{"thread_id":"thread_123","run_list":[{"run_id":"run_456","state":4}]}}`, "failed", "模拟接口错误 log_id=fixture-log", true},
|
|
707
|
+
{"succeeded without artifacts", `{"thread":{"thread_id":"thread_123","run_list":[{"run_id":"run_456","state":3}]}}`, "succeeded", "下载失败:未找到可下载的产物", true},
|
|
708
|
+
{"canceled", `{"thread":{"thread_id":"thread_123","run_list":[{"run_id":"run_456","state":5}]}}`, "canceled", "", true},
|
|
709
|
+
{"running", `{"thread":{"thread_id":"thread_123","run_list":[{"run_id":"run_456","state":2}]}}`, "running", "", false},
|
|
710
|
+
{"different run", `{"thread":{"thread_id":"thread_123","run_list":[{"run_id":"other","state":4}]}}`, "unknown", "模拟接口错误 log_id=fixture-log", false},
|
|
711
|
+
{"missing thread", `{}`, "unknown", "模拟接口错误 log_id=fixture-log", false},
|
|
712
|
+
{"null data", `null`, "unknown", "模拟接口错误 log_id=fixture-log", false},
|
|
713
|
+
{"malformed run", `{"thread":{"run_list":"invalid"}}`, "unknown", "模拟接口错误 log_id=fixture-log", false},
|
|
714
|
+
} {
|
|
715
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
716
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
717
|
+
if r.URL.Path != "/api/biz/v1/skill/get_thread" {
|
|
718
|
+
t.Fatalf("unexpected path %s", r.URL.Path)
|
|
719
|
+
}
|
|
720
|
+
fmt.Fprintf(w, `{"ret":"2","errmsg":"模拟接口错误","log_id":"fixture-log","data":%s}`, tt.data)
|
|
721
|
+
}))
|
|
722
|
+
defer server.Close()
|
|
723
|
+
var stdout, stderr bytes.Buffer
|
|
724
|
+
root := newTestRootCommand(t, &stdout, &stderr, server.URL)
|
|
725
|
+
root.SetArgs([]string{"query-result", "--thread-id", "thread_123", "--run-id", "run_456", "--download-dir", t.TempDir()})
|
|
726
|
+
if err := root.Execute(); err != nil {
|
|
727
|
+
t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
|
|
728
|
+
}
|
|
729
|
+
got := decodeJSON(t, stdout.Bytes())
|
|
730
|
+
if got["status"] != tt.status || got["completed"] != tt.completed || got["error_message"] != tt.message {
|
|
731
|
+
t.Fatalf("output = %#v, want status=%s completed=%v error_message=%q", got, tt.status, tt.completed, tt.message)
|
|
732
|
+
}
|
|
733
|
+
if got["thread_id"] != "thread_123" || got["run_id"] != "run_456" {
|
|
734
|
+
t.Fatalf("output = %#v, want requested thread/run IDs", got)
|
|
735
|
+
}
|
|
736
|
+
})
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
695
740
|
func TestQueryResultFailedReturnsErrorMessage(t *testing.T) {
|
|
696
741
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
697
742
|
switch r.URL.Path {
|
|
@@ -789,10 +834,10 @@ func TestQueryResultRunStateMapping(t *testing.T) {
|
|
|
789
834
|
{2, "running", false},
|
|
790
835
|
{4, "failed", true},
|
|
791
836
|
{5, "canceled", true},
|
|
792
|
-
{6, "
|
|
837
|
+
{6, "input_required", false},
|
|
793
838
|
{7, "running", false},
|
|
794
839
|
{8, "running", false},
|
|
795
|
-
{9, "
|
|
840
|
+
{9, "interrupted", true},
|
|
796
841
|
{99, "unknown", false},
|
|
797
842
|
}
|
|
798
843
|
for _, tt := range tests {
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"net/http"
|
|
8
|
+
"net/http/httptest"
|
|
9
|
+
"os"
|
|
10
|
+
"path/filepath"
|
|
11
|
+
"strings"
|
|
12
|
+
"testing"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// This matrix follows the legacy skill query endpoint, including its middleware.
|
|
16
|
+
// Business error envelopes deliberately omit Run, as the real handler does.
|
|
17
|
+
func TestQueryResultResponseBranches(t *testing.T) {
|
|
18
|
+
tests := []struct {
|
|
19
|
+
name, ret, message, status string
|
|
20
|
+
httpStatus int
|
|
21
|
+
}{
|
|
22
|
+
{"empty thread id", "2", "thread id不能为空", "unknown", 200},
|
|
23
|
+
{"invalid version", "2", "version参数无效", "unknown", 200},
|
|
24
|
+
{"empty run id", "2", "run id不能为空", "unknown", 200},
|
|
25
|
+
{"invalid AK owner info", "2", "无法根据此Ak找到有效用户信息", "unknown", 200},
|
|
26
|
+
{"invalid OAuth user info", "2", "无法获取有效用户信息", "unknown", 200},
|
|
27
|
+
{"RPC or DB error", "5", "获取会话信息失败", "unknown", 200},
|
|
28
|
+
{"no runs", "1", "会话中缺少有效的Run", "unknown", 200},
|
|
29
|
+
{"run not found", "2", "会话中未找到RunID为run_456的Run", "unknown", 200},
|
|
30
|
+
{"run copy failed", "1", "解析Run信息失败", "unknown", 200},
|
|
31
|
+
{"failed without reason", "5", "创作失败", "failed", 200},
|
|
32
|
+
{"failed with reason", "5", "创作失败:视频可能不合规,请修改后重试", "failed", 200},
|
|
33
|
+
{"failed with empty reason", "5", "创作失败:", "failed", 200},
|
|
34
|
+
{"canceled", "5", "创作已取消", "canceled", 200},
|
|
35
|
+
{"bad bearer or AK", "1015", "invalid access key", "unknown", 401},
|
|
36
|
+
{"bad owner", "1015", "invalid access key owner", "unknown", 401},
|
|
37
|
+
{"bad team id", "1015", "invalid team id", "unknown", 401},
|
|
38
|
+
{"team login missing", "1015", "login required for team access", "unknown", 401},
|
|
39
|
+
{"permission RPC failure", "1015", "team permission check failed", "unknown", 401},
|
|
40
|
+
{"permission RPC rejection", "1015", "team permission denied", "unknown", 401},
|
|
41
|
+
{"team denied", "16019", "team permission denied", "unknown", 200},
|
|
42
|
+
{"member inactive", "16020", "team member is inactive", "unknown", 200},
|
|
43
|
+
{"role denied", "16021", "team role is not allowed", "unknown", 200},
|
|
44
|
+
{"team missing", "16022", "team not found", "unknown", 200},
|
|
45
|
+
{"team unavailable", "16023", "team is unavailable", "unknown", 200},
|
|
46
|
+
{"subscription inactive", "16024", "team subscription is inactive", "unknown", 200},
|
|
47
|
+
{"member limit", "16025", "team member limit exceeded", "unknown", 200},
|
|
48
|
+
{"product identity invalid", "2", "missing or unsupported business app id", "unknown", 400},
|
|
49
|
+
{"product identity unavailable", "5", "business app identity unavailable", "unknown", 503},
|
|
50
|
+
{"request binding error", "-3", "bad request", "unknown", 200},
|
|
51
|
+
{"panic", "-2", "system panic", "unknown", 500},
|
|
52
|
+
{"unknown future ret", "987654", "new server error", "unknown", 200},
|
|
53
|
+
{"wrong ret with failure phrase", "2", "创作失败:原因", "unknown", 200},
|
|
54
|
+
{"wrong ret with cancellation phrase", "1", "创作已取消", "unknown", 200},
|
|
55
|
+
{"unrecognized ret with failure phrase", "987654", "创作失败", "unknown", 200},
|
|
56
|
+
{"zero ret without run", "0", "创作失败", "unknown", 200},
|
|
57
|
+
{"contains failure word", "5", "查询创作失败记录失败", "unknown", 200},
|
|
58
|
+
{"only moderation wording", "5", "视频可能不合规", "unknown", 200},
|
|
59
|
+
{"wrong failure delimiter", "5", "创作失败: reason", "unknown", 200},
|
|
60
|
+
{"cancellation suffix", "5", "创作已取消查询失败", "unknown", 200},
|
|
61
|
+
}
|
|
62
|
+
for _, tt := range tests {
|
|
63
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
64
|
+
body, err := json.Marshal(map[string]any{"ret": tt.ret, "errmsg": tt.message, "log_id": "branch-log", "data": nil})
|
|
65
|
+
if err != nil {
|
|
66
|
+
t.Fatal(err)
|
|
67
|
+
}
|
|
68
|
+
got := executeQueryResponse(t, tt.httpStatus, string(body))
|
|
69
|
+
completed := tt.status == "failed" || tt.status == "canceled"
|
|
70
|
+
if got["status"] != tt.status || got["completed"] != completed {
|
|
71
|
+
t.Fatalf("got %#v, want %s completed=%t", got, tt.status, completed)
|
|
72
|
+
}
|
|
73
|
+
message, _ := got["error_message"].(string)
|
|
74
|
+
if tt.ret != "0" && (!strings.Contains(message, tt.message) || !strings.Contains(message, "branch-log")) {
|
|
75
|
+
t.Fatalf("original error/LogID lost: %q", message)
|
|
76
|
+
}
|
|
77
|
+
if message == "" {
|
|
78
|
+
t.Fatalf("missing error: %#v", got)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func executeQueryResponse(t *testing.T, status int, body string) map[string]any {
|
|
85
|
+
t.Helper()
|
|
86
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
87
|
+
if r.URL.Path != "/api/biz/v1/skill/get_thread" {
|
|
88
|
+
t.Errorf("unexpected download/request: %s", r.URL.Path)
|
|
89
|
+
}
|
|
90
|
+
w.WriteHeader(status)
|
|
91
|
+
_, _ = w.Write([]byte(body))
|
|
92
|
+
}))
|
|
93
|
+
defer server.Close()
|
|
94
|
+
return executeQueryURL(t, server.URL, t.TempDir())
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func executeQueryURL(t *testing.T, url, dir string) map[string]any {
|
|
98
|
+
t.Helper()
|
|
99
|
+
var stdout, stderr bytes.Buffer
|
|
100
|
+
root := newTestRootCommand(t, &stdout, &stderr, url)
|
|
101
|
+
root.SetArgs([]string{"query-result", "--thread-id", "thread_123", "--run-id", "run_456", "--download-dir", dir})
|
|
102
|
+
if err := root.Execute(); err != nil {
|
|
103
|
+
t.Fatalf("Execute: %v, stderr=%s", err, stderr.String())
|
|
104
|
+
}
|
|
105
|
+
got := decodeJSON(t, stdout.Bytes())
|
|
106
|
+
if got["thread_id"] != "thread_123" || got["run_id"] != "run_456" {
|
|
107
|
+
t.Fatalf("requested IDs lost: %#v", got)
|
|
108
|
+
}
|
|
109
|
+
for _, key := range []string{"videos", "images"} {
|
|
110
|
+
if _, ok := got[key].([]any); !ok {
|
|
111
|
+
t.Fatalf("%s is not an array: %#v", key, got)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return got
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func TestQueryResultRecordedFailureResponse(t *testing.T) {
|
|
118
|
+
// Recorded production response; only diagnostic identifiers are sanitized.
|
|
119
|
+
data, err := os.ReadFile("testdata/query-result/recorded-failure.json")
|
|
120
|
+
if err != nil {
|
|
121
|
+
t.Fatal(err)
|
|
122
|
+
}
|
|
123
|
+
got := executeQueryResponse(t, 200, string(data))
|
|
124
|
+
if got["status"] != "failed" || got["completed"] != true || got["error_message"] != "创作失败:视频可能不合规,请修改后重试 log_id=recorded-failure-log" {
|
|
125
|
+
t.Fatalf("recorded failure was not recognized: %#v", got)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func TestQueryResultRunPrecedesEveryBusinessError(t *testing.T) {
|
|
130
|
+
states := []struct {
|
|
131
|
+
state int
|
|
132
|
+
status string
|
|
133
|
+
completed bool
|
|
134
|
+
}{
|
|
135
|
+
{0, "unknown", false}, {1, "running", false}, {2, "running", false},
|
|
136
|
+
{3, "succeeded", true}, {4, "failed", true}, {5, "canceled", true},
|
|
137
|
+
{6, "input_required", false}, {7, "running", false}, {8, "running", false},
|
|
138
|
+
{9, "interrupted", true}, {99, "unknown", false},
|
|
139
|
+
}
|
|
140
|
+
// These are compatibility envelopes, not assertions that the server currently
|
|
141
|
+
// returns a structured Run for every error. The recorded fixture covers that.
|
|
142
|
+
for _, ret := range []string{"0", "1", "2", "5", "1015", "16019", "16020", "16021", "16022", "16023", "16024", "16025", "-2", "-3", "987654"} {
|
|
143
|
+
for _, state := range states {
|
|
144
|
+
t.Run(fmt.Sprintf("ret=%s/state=%d", ret, state.state), func(t *testing.T) {
|
|
145
|
+
clearDailyErrorLog(t)
|
|
146
|
+
got := executeQueryResponse(t, 200, fmt.Sprintf(`{"ret":%q,"errmsg":"创作失败:envelope reason","log_id":"matrix-log","data":{"thread":{"run_list":[{"run_id":"other","state":4},{"run_id":"run_456","state":%d,"fail_reason":{"message":"Run reason","code":12023}}]}}}`, ret, state.state))
|
|
147
|
+
if got["status"] != state.status || got["completed"] != state.completed {
|
|
148
|
+
t.Fatalf("got %#v, want %#v", got, state)
|
|
149
|
+
}
|
|
150
|
+
if state.state == 4 && got["error_message"] != "Run reason (error_code=12023)" {
|
|
151
|
+
t.Fatalf("lost run reason: %#v", got)
|
|
152
|
+
}
|
|
153
|
+
if state.state != 3 && state.state != 4 && got["error_message"] != "" {
|
|
154
|
+
t.Fatalf("envelope overwrote Run: %#v", got)
|
|
155
|
+
}
|
|
156
|
+
if ret != "0" {
|
|
157
|
+
logs, err := json.Marshal(readDailyErrorLog(t))
|
|
158
|
+
if err != nil {
|
|
159
|
+
t.Fatal(err)
|
|
160
|
+
}
|
|
161
|
+
if !strings.Contains(string(logs), "ret="+ret) || !strings.Contains(string(logs), "matrix-log") {
|
|
162
|
+
t.Fatalf("missing diagnostics: %s", logs)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
func TestQueryResultUnusableResponses(t *testing.T) {
|
|
171
|
+
for _, body := range []string{
|
|
172
|
+
``, `<html>gateway error</html>`, `{"ret":`,
|
|
173
|
+
`{}`, `{"ret":"0"}`, `{"ret":"0","data":null}`,
|
|
174
|
+
`{"ret":"0","data":{}}`, `{"ret":"0","data":{"readable_text":"失败"}}`,
|
|
175
|
+
`{"ret":"0","data":{"thread":null}}`,
|
|
176
|
+
`{"ret":"0","data":{"thread":{"run_list":[]}}}`,
|
|
177
|
+
`{"ret":"0","data":{"thread":{"run_list":"invalid"}}}`,
|
|
178
|
+
`{"ret":"0","data":{"thread":{"run_list":[{"run_id":"other","state":4}]}}}`,
|
|
179
|
+
`{"ret":"0","data":{"thread":{"run_list":[{"run_id":"run_456","state":"bad"}]}}}`,
|
|
180
|
+
`{"ret":"","errmsg":"创作失败","data":null}`,
|
|
181
|
+
`{"ret":"5","errmsg":"","data":null}`,
|
|
182
|
+
} {
|
|
183
|
+
t.Run(body, func(t *testing.T) {
|
|
184
|
+
got := executeQueryResponse(t, 200, body)
|
|
185
|
+
if got["status"] != "unknown" || got["completed"] != false || got["error_message"] == "" {
|
|
186
|
+
t.Fatalf("got %#v", got)
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
for _, code := range []int{400, 401, 403, 429, 500, 502, 503, 504} {
|
|
191
|
+
t.Run(fmt.Sprint(code), func(t *testing.T) {
|
|
192
|
+
got := executeQueryResponse(t, code, "gateway error")
|
|
193
|
+
if got["status"] != "unknown" || got["completed"] != false || !strings.Contains(got["error_message"].(string), fmt.Sprint(code)) {
|
|
194
|
+
t.Fatalf("got %#v", got)
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
t.Run("connection refused", func(t *testing.T) {
|
|
199
|
+
server := httptest.NewServer(http.NotFoundHandler())
|
|
200
|
+
server.Close()
|
|
201
|
+
got := executeQueryURL(t, server.URL, t.TempDir())
|
|
202
|
+
if got["status"] != "unknown" || got["completed"] != false || got["error_message"] == "" {
|
|
203
|
+
t.Fatalf("got %#v", got)
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
func TestQueryResultRetainsPartialDownloads(t *testing.T) {
|
|
209
|
+
for _, mode := range []string{"second video fails", "first image fails", "second image fails", "cached video then image fails"} {
|
|
210
|
+
t.Run(mode, func(t *testing.T) {
|
|
211
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
212
|
+
if r.URL.Path == "/api/biz/v1/skill/get_thread" {
|
|
213
|
+
content := []any{map[string]any{"sub_type": "biz/x_data_video", "data": map[string]any{"video": map[string]any{"download_url": serverURL(r) + "/video", "vid": "first"}}}}
|
|
214
|
+
badVideo := map[string]any{"sub_type": "biz/x_data_video", "data": map[string]any{"video": map[string]any{"download_url": "invalid-scheme://broken"}}}
|
|
215
|
+
badImage := map[string]any{"sub_type": "biz/x_data_image", "data": map[string]any{"image": map[string]any{"url": "invalid-scheme://broken"}}}
|
|
216
|
+
if mode == "second video fails" {
|
|
217
|
+
content = append(content, badVideo)
|
|
218
|
+
} else {
|
|
219
|
+
if mode == "second image fails" {
|
|
220
|
+
content = append(content, map[string]any{"sub_type": "biz/x_data_image", "data": map[string]any{"image": map[string]any{"url": serverURL(r) + "/image", "asset_id": "first-image"}}})
|
|
221
|
+
}
|
|
222
|
+
content = append(content, badImage)
|
|
223
|
+
}
|
|
224
|
+
_ = json.NewEncoder(w).Encode(map[string]any{"ret": "0", "data": map[string]any{"thread": map[string]any{"run_list": []any{map[string]any{"run_id": "run_456", "state": 3, "entry_list": []any{map[string]any{"artifact": map[string]any{"content": content}}}}}}}})
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
_, _ = w.Write([]byte("media-data"))
|
|
228
|
+
}))
|
|
229
|
+
defer server.Close()
|
|
230
|
+
dir := t.TempDir()
|
|
231
|
+
if mode == "cached video then image fails" {
|
|
232
|
+
if err := os.WriteFile(filepath.Join(dir, "first.mp4"), []byte("media-data"), 0600); err != nil {
|
|
233
|
+
t.Fatal(err)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
got := executeQueryURL(t, server.URL, dir)
|
|
237
|
+
if got["status"] != "succeeded" || got["completed"] != true || !strings.Contains(got["error_message"].(string), "下载失败") {
|
|
238
|
+
t.Fatalf("got %#v", got)
|
|
239
|
+
}
|
|
240
|
+
videos := got["videos"].([]any)
|
|
241
|
+
images := got["images"].([]any)
|
|
242
|
+
wantImages := 0
|
|
243
|
+
if mode == "second image fails" {
|
|
244
|
+
wantImages = 1
|
|
245
|
+
}
|
|
246
|
+
if len(videos) != 1 || len(images) != wantImages {
|
|
247
|
+
t.Fatalf("partial files lost: %#v", got)
|
|
248
|
+
}
|
|
249
|
+
for _, file := range append(videos, images...) {
|
|
250
|
+
assertFileContent(t, file.(map[string]any)["output_path"].(string), "media-data")
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
func TestQueryResultTerminalEnvelopeWithoutUsableRun(t *testing.T) {
|
|
257
|
+
for _, data := range []string{`null`, `{}`, `{"thread":null}`, `{"thread":{"run_list":[]}}`, `{"thread":{"run_list":[{"run_id":"other","state":3}]}}`, `{"thread":{"run_list":"bad"}}`} {
|
|
258
|
+
t.Run(data, func(t *testing.T) {
|
|
259
|
+
got := executeQueryResponse(t, 200, `{"ret":"5","errmsg":"创作失败:reason","data":`+data+`}`)
|
|
260
|
+
if got["status"] != "failed" || got["completed"] != true || got["error_message"] != "创作失败:reason" {
|
|
261
|
+
t.Fatalf("got %#v", got)
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
}
|
package/cmd/root.go
CHANGED
|
@@ -119,6 +119,7 @@ func newRootCommandWithAdapter(stdout, stderr io.Writer, runner *common.Runner,
|
|
|
119
119
|
root.AddCommand(canvascmd.NewCommand(stdout, stderr, runner))
|
|
120
120
|
root.AddCommand(newDownloadResultCommand(stdout, stderr, runner))
|
|
121
121
|
root.AddCommand(newGetCreditBalanceCommand(stdout, stderr, runner))
|
|
122
|
+
root.AddCommand(newWatermarkCommand(stdout, stderr, runner))
|
|
122
123
|
root.AddCommand(newModelCommand(stdout, stderr, runner))
|
|
123
124
|
root.AddCommand(newMarketingCommand(stdout, stderr, runner, policy))
|
|
124
125
|
root.AddCommand(newGetThreadCommand(stdout, stderr, runner))
|
package/cmd/space.go
CHANGED
|
@@ -30,7 +30,7 @@ func prepareCommandSpace(cmd *cobra.Command, runner *common.Runner, stderr io.Wr
|
|
|
30
30
|
top = top.Parent()
|
|
31
31
|
}
|
|
32
32
|
switch top.Name() {
|
|
33
|
-
case commandnames.Space, commandnames.Login, commandnames.Logout, commandnames.Status, commandnames.Update, commandnames.Completion, commandnames.Help, commandnames.DownloadResult:
|
|
33
|
+
case commandnames.Space, commandnames.Login, commandnames.Logout, commandnames.Status, commandnames.Update, commandnames.Completion, commandnames.Help, commandnames.DownloadResult, commandnames.Watermark:
|
|
34
34
|
return nil
|
|
35
35
|
}
|
|
36
36
|
raw, _ := cmd.Flags().GetString("space-context")
|