@pippit-dev/cli 1.0.45 → 1.0.47

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
@@ -149,6 +149,12 @@ pippit-tool-cli --version
149
149
  4. 验证通过后,在 GitHub Actions 的 `Promote canary to latest` workflow 中点击 `Run workflow`,填写已验证的 `X.Y.Z`。该 workflow 须先合入默认分支才会显示手动入口,并复用仓库已有的 `NPM_TOKEN`。
150
150
  5. 提升流程要求输入版本与当前 `canary` 一致,只移动 npm `latest` 指针并回读确认,不重新构建或发布 npm 包。如果 canary 已更新,需要验证新的候选版本后再提升。
151
151
 
152
+ GitHub Release 已成功、npm 发布失败时的补发步骤:
153
+
154
+ 1. 在 GitHub Actions 选择 `Republish npm from Release`,点击 `Run workflow`,填写已有的 `vX.Y.Z` tag。该 workflow 须先合入默认分支,复用已有 `NPM_TOKEN`。
155
+ 2. 流程检出指定 tag,校验源码版本、已发布的六个平台产物及 `checksums.txt`,复用这些二进制,只准备并发布 npm canary,不运行 GoReleaser。
156
+ 3. 目标必须高于当前 latest 和 canary,且 npm 上尚无该版本;发布前查询异常会停止。npm 接收发布后可能暂时返回 404,流程会有上限地重试读取版本与渠道,确认 canary 指向目标版本、latest 保持原值。回读超时只表示结果尚未确认,先核实 npm 实际处理状态,不连续重复发布,也不删除已有 Release 或重打 tag。
157
+
152
158
  维护者回滚步骤:
153
159
 
154
160
  1. 在 [GitHub Actions](https://github.com/Pippit-dev/cli/actions) 选择 `Rollback CLI`,点击 `Run workflow`。该 workflow 须先合入默认分支;复用 `NPM_TOKEN` 修改 npm 标签,使用只读 `GITHUB_TOKEN` 检查 Release。
@@ -157,7 +163,7 @@ pippit-tool-cli --version
157
163
  4. 写入前再次核对当前渠道,随后只移动所选 npm 标签并回读确认。不会重新构建、发包、修改 Git tag 或改变另一渠道。Actions 日志和运行摘要会记录目标及确认结果;写入或回读失败时,先用 `npm view @pippit-dev/cli dist-tags --prefer-online` 核实实际状态,再决定是否重试。
158
164
  5. 已安装问题版本的用户需要主动执行 `pippit-tool-cli update --channel stable`(回滚 canary 时使用 `--channel canary`)并核对 `pippit-tool-cli --version`。旧 CLI 可执行默认 `update` 获取 `latest`,但须确保未设置 `PIPPIT_CLI_INSTALL_PACKAGE` 覆盖变量。已运行的进程及 Skill 独立缓存中的 CLI 不会被渠道回滚自动替换。
159
165
 
160
- 发布、提升和回滚共用并发组,避免这些 workflow 同时修改渠道;不取消正在运行的任务。GitHub 默认只保留一个等待中的同组任务,连续触发时请检查是否有等待任务被替换。并发组不能锁住维护者在终端执行的 npm 写入,操作期间应避免其它渠道修改。此阶段不提供百分比放量或自动回滚,普通客户端也不会因指针变化而自动安装;显式指定问题版本的安装仍然可用。
166
+ 发布、补发、提升和回滚共用并发组,避免这些 workflow 同时修改渠道;不取消正在运行的任务。GitHub 默认只保留一个等待中的同组任务,连续触发时请检查是否有等待任务被替换。并发组不能锁住维护者在终端执行的 npm 写入,操作期间应避免其它渠道修改。此阶段不提供百分比放量或自动回滚,普通客户端也不会因指针变化而自动安装;显式指定问题版本的安装仍然可用。
161
167
 
162
168
  ## 短剧工作流技能
163
169
 
package/checksums.txt CHANGED
@@ -1,6 +1,6 @@
1
- 3802437e93dcd395df47e4e735a8a4b958528f76ea64d672cd3fa3cb719fd163 pippit-tool-cli-1.0.45-darwin-amd64.tar.gz
2
- b80b030e77b0fbdffb0b05c8a38fdc5c939d01ca69cb1ab6d2c0bd0c1e236892 pippit-tool-cli-1.0.45-darwin-arm64.tar.gz
3
- 33095e1ceec2220ea1b79abfa56948dd5d237c1831ab2437c8af064d00db7c99 pippit-tool-cli-1.0.45-linux-amd64.tar.gz
4
- 85a91aeae7b9ea1324a8aee1be576fad12297cecdfe2828851147d50f4dbc177 pippit-tool-cli-1.0.45-linux-arm64.tar.gz
5
- e93240dbc814104ea278197aff983c2491089be1b9fe55f216007b1a612c7f93 pippit-tool-cli-1.0.45-windows-amd64.zip
6
- d5f196ea5b5f466911573a1177702ad00f27da153d2f29b5b1e65cd60f7f5100 pippit-tool-cli-1.0.45-windows-arm64.zip
1
+ 966d956f11b6922cd1112cc8e969578d7faaa093150676dc8fbcad1988ec185e pippit-tool-cli-1.0.47-darwin-amd64.tar.gz
2
+ 5bf74cd01214b76bc046c090e5dcdb8cccde9ac0af8ca1bcb9a01f45b7d7377f pippit-tool-cli-1.0.47-darwin-arm64.tar.gz
3
+ e043d77d2c7869b245081179abce175d9bd42608a3549205c143e7bc9aef76db pippit-tool-cli-1.0.47-linux-amd64.tar.gz
4
+ 3a1bd95631363c4702004da2b823ae34baf37576324eca5501d9e832ba8773cc pippit-tool-cli-1.0.47-linux-arm64.tar.gz
5
+ 3e0a59ff3f467ec1318c128d97fc33ec952b26715c4053c5abae27ee365f6f1d pippit-tool-cli-1.0.47-windows-amd64.zip
6
+ cbab4b7a34567a30b08abf2b2856958d0813aca6515b1a8edcead5939b47610c pippit-tool-cli-1.0.47-windows-arm64.zip
@@ -8,14 +8,18 @@ import (
8
8
  "os"
9
9
  "path/filepath"
10
10
  "strings"
11
+ "sync/atomic"
11
12
  "testing"
12
13
 
13
14
  "github.com/bytedance/sonic"
14
15
  )
15
16
 
16
17
  func TestGenerateVideo(t *testing.T) {
17
- assetIDs := []string{"image_asset_1", "image_asset_2", "video_asset_1", "video_asset_2", "audio_asset_1"}
18
- uploadIndex := 0
18
+ assetIDs := map[string]string{
19
+ "cat1.jpg": "image_asset_1", "cat2.jpg": "image_asset_2",
20
+ "video1.mp4": "video_asset_1", "video2.mp4": "video_asset_2", "bgm.wav": "audio_asset_1",
21
+ }
22
+ var uploadCount atomic.Int32
19
23
 
20
24
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
21
25
  if r.Header.Get("Authorization") != "Bearer test-token" {
@@ -26,9 +30,6 @@ func TestGenerateVideo(t *testing.T) {
26
30
  if r.Method != http.MethodPost {
27
31
  t.Fatalf("upload method = %s, want POST", r.Method)
28
32
  }
29
- if uploadIndex >= len(assetIDs) {
30
- t.Fatalf("unexpected upload %d", uploadIndex)
31
- }
32
33
  if err := r.ParseMultipartForm(1 << 20); err != nil {
33
34
  t.Fatalf("ParseMultipartForm(): %v", err)
34
35
  }
@@ -39,11 +40,15 @@ func TestGenerateVideo(t *testing.T) {
39
40
  if files[0].Filename == "bgm.wav" && !strings.HasPrefix(files[0].Header.Get("Content-Type"), "audio/") {
40
41
  t.Fatalf("audio content type = %q, want audio content type", files[0].Header.Get("Content-Type"))
41
42
  }
42
- uploadIndex++
43
- _, _ = w.Write([]byte(`{"ret":"0","errmsg":"","data":{"pippit_asset_id":"` + assetIDs[uploadIndex-1] + `"}}`))
43
+ assetID, ok := assetIDs[files[0].Filename]
44
+ if !ok {
45
+ t.Errorf("unexpected upload %q", files[0].Filename)
46
+ }
47
+ uploadCount.Add(1)
48
+ _, _ = w.Write([]byte(`{"ret":"0","errmsg":"","data":{"pippit_asset_id":"` + assetID + `"}}`))
44
49
  case "/api/biz/v1/skill/submit_run":
45
- if uploadIndex != len(assetIDs) {
46
- t.Fatalf("submit called after %d uploads, want %d", uploadIndex, len(assetIDs))
50
+ if got := uploadCount.Load(); int(got) != len(assetIDs) {
51
+ t.Fatalf("submit called after %d uploads, want %d", got, len(assetIDs))
47
52
  }
48
53
  data, err := io.ReadAll(r.Body)
49
54
  if err != nil {
@@ -288,19 +293,24 @@ func TestGenerateVideoAcceptsReferencesBeyondFormerLimits(t *testing.T) {
288
293
  }
289
294
  }
290
295
 
291
- uploadIndex := 0
296
+ var uploadCount atomic.Int32
292
297
  submitted := false
293
298
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
294
299
  switch r.URL.Path {
295
300
  case "/api/biz/v1/skill/upload_file":
296
- if uploadIndex >= len(assetIDs) {
297
- t.Fatalf("unexpected upload %d", uploadIndex)
298
- }
299
- _, _ = w.Write([]byte(`{"ret":"0","data":{"pippit_asset_id":"` + assetIDs[uploadIndex] + `"}}`))
300
- uploadIndex++
301
+ file, header, err := r.FormFile("file")
302
+ if err != nil {
303
+ t.Errorf("FormFile(): %v", err)
304
+ http.Error(w, "invalid upload", http.StatusBadRequest)
305
+ return
306
+ }
307
+ file.Close()
308
+ defer r.MultipartForm.RemoveAll()
309
+ uploadCount.Add(1)
310
+ _, _ = w.Write([]byte(`{"ret":"0","data":{"pippit_asset_id":"` + header.Filename + `"}}`))
301
311
  case "/api/biz/v1/skill/submit_run":
302
- if uploadIndex != len(assetIDs) {
303
- t.Fatalf("uploaded %d references, want %d", uploadIndex, len(assetIDs))
312
+ if got := uploadCount.Load(); int(got) != len(assetIDs) {
313
+ t.Fatalf("uploaded %d references, want %d", got, len(assetIDs))
304
314
  }
305
315
  data, err := io.ReadAll(r.Body)
306
316
  if err != nil {
@@ -0,0 +1,132 @@
1
+ package cmd
2
+
3
+ import (
4
+ "bytes"
5
+ "errors"
6
+ "fmt"
7
+ "net/http"
8
+ "net/http/httptest"
9
+ "os"
10
+ "path/filepath"
11
+ "strings"
12
+ "sync"
13
+ "sync/atomic"
14
+ "testing"
15
+
16
+ "github.com/Pippit-dev/pippit-cli/internal/common"
17
+ "github.com/Pippit-dev/pippit-cli/internal/config"
18
+ )
19
+
20
+ func TestGenerationCollectsUploadFailuresBeforeReturning(t *testing.T) {
21
+ for _, command := range []string{"generate-image", "generate-video"} {
22
+ t.Run(command, func(t *testing.T) {
23
+ t.Setenv("HOME", t.TempDir())
24
+ t.Setenv("XDG_CACHE_HOME", t.TempDir())
25
+ t.Setenv("LocalAppData", t.TempDir())
26
+ type material struct {
27
+ flag, name, failure string
28
+ missing bool
29
+ }
30
+ materials := []material{
31
+ {flag: "image", name: "rejected-a.jpg", failure: `第1张参考图片 "rejected-a.jpg" 上传失败: 上传文件请求返回失败: ret=1 errmsg=图片不符合社区规范 log_id=log-rejected-a.jpg`},
32
+ {flag: "image", name: "accepted.jpg"},
33
+ {flag: "image", name: "rejected-b.jpg", failure: `第3张参考图片 "rejected-b.jpg" 上传失败: 上传文件请求返回失败: ret=1 errmsg=图片不符合社区规范 log_id=log-rejected-b.jpg`},
34
+ }
35
+ if command == "generate-video" {
36
+ materials = append(materials,
37
+ material{flag: "video", name: "rejected.mp4", failure: `第1个参考视频 "rejected.mp4" 上传失败: 上传文件请求返回失败: ret=1 errmsg=视频不符合社区规范 log_id=log-rejected.mp4`},
38
+ material{flag: "video", name: "accepted.mp4"},
39
+ material{flag: "audio", name: "missing.wav", missing: true, failure: `第1段参考音频 "missing.wav" 上传失败: 上传文件不存在:`},
40
+ material{flag: "audio", name: "unavailable.wav", failure: `第2段参考音频 "unavailable.wav" 上传失败: 上传文件请求返回失败: ret=1 errmsg=服务暂不可用 log_id=log-unavailable.wav`},
41
+ )
42
+ }
43
+ args := []string{command, "--prompt", "test"}
44
+ if command == "generate-image" {
45
+ args = append(args, "--model", "Seedream 5.0 Pro")
46
+ }
47
+ dir := t.TempDir()
48
+ for _, item := range materials {
49
+ path := filepath.Join(dir, item.name)
50
+ if !item.missing {
51
+ if err := os.WriteFile(path, []byte("media"), 0600); err != nil {
52
+ t.Fatal(err)
53
+ }
54
+ }
55
+ args = append(args, "--"+item.flag, path)
56
+ }
57
+ var mu sync.Mutex
58
+ seen := make(map[string]int)
59
+ var submits atomic.Int32
60
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
61
+ switch r.URL.Path {
62
+ case config.GetAvailableModelListPath:
63
+ _, _ = w.Write([]byte(`{"ret":"0","data":{"scene":"web_image_agent","config_key":"image-key","config":{"models":[{"key":"seedream_5.0_pro","name":"Seedream 5.0 Pro","kind":"image"}]}}}`))
64
+ case config.UploadFilePath:
65
+ file, header, err := r.FormFile("file")
66
+ if err != nil {
67
+ t.Errorf("FormFile(): %v", err)
68
+ http.Error(w, "invalid upload", http.StatusBadRequest)
69
+ return
70
+ }
71
+ file.Close()
72
+ defer r.MultipartForm.RemoveAll()
73
+ mu.Lock()
74
+ seen[header.Filename]++
75
+ mu.Unlock()
76
+ switch {
77
+ case strings.HasPrefix(header.Filename, "accepted"):
78
+ fmt.Fprintf(w, `{"ret":"0","data":{"pippit_asset_id":%q}}`, header.Filename)
79
+ case header.Filename == "unavailable.wav":
80
+ fmt.Fprintf(w, `{"ret":"1","errmsg":"服务暂不可用","log_id":%q}`, "log-"+header.Filename)
81
+ default:
82
+ reason := "图片不符合社区规范"
83
+ if strings.HasSuffix(header.Filename, ".mp4") {
84
+ reason = "视频不符合社区规范"
85
+ }
86
+ fmt.Fprintf(w, `{"ret":"1","errmsg":%q,"log_id":%q}`, reason, "log-"+header.Filename)
87
+ }
88
+ case config.SubmitRunPath:
89
+ submits.Add(1)
90
+ http.Error(w, "must not submit", http.StatusInternalServerError)
91
+ default:
92
+ t.Errorf("unexpected request %s", r.URL.Path)
93
+ http.NotFound(w, r)
94
+ }
95
+ }))
96
+ defer server.Close()
97
+ var stdout, stderr bytes.Buffer
98
+ root := newTestRootCommand(t, &stdout, &stderr, server.URL)
99
+ root.SetArgs(args)
100
+ err := root.Execute()
101
+ if err == nil || !strings.Contains(err.Error(), "未提交生成") {
102
+ t.Fatalf("error = %v, want upload failure summary", err)
103
+ }
104
+ var logErr *common.LogIDError
105
+ if !errors.As(err, &logErr) || logErr.LogID() != "log-rejected-a.jpg" {
106
+ t.Errorf("aggregated error lost the original LogID error chain: %v", err)
107
+ }
108
+ lastPosition := -1
109
+ mu.Lock()
110
+ defer mu.Unlock()
111
+ for _, item := range materials {
112
+ wantRequests := 1
113
+ if item.missing {
114
+ wantRequests = 0
115
+ }
116
+ if seen[item.name] != wantRequests {
117
+ t.Errorf("%s uploaded %d times, want %d", item.name, seen[item.name], wantRequests)
118
+ }
119
+ if item.failure != "" {
120
+ position := strings.Index(err.Error(), item.failure)
121
+ if position <= lastPosition {
122
+ t.Errorf("failure missing or out of order: %s; error: %v", item.failure, err)
123
+ }
124
+ lastPosition = position
125
+ }
126
+ }
127
+ if submits.Load() != 0 || stdout.Len() != 0 {
128
+ t.Fatalf("failed uploads created a task: submits=%d stdout=%s", submits.Load(), stdout.String())
129
+ }
130
+ })
131
+ }
132
+ }
@@ -1,6 +1,6 @@
1
- 3802437e93dcd395df47e4e735a8a4b958528f76ea64d672cd3fa3cb719fd163 pippit-tool-cli-1.0.45-darwin-amd64.tar.gz
2
- b80b030e77b0fbdffb0b05c8a38fdc5c939d01ca69cb1ab6d2c0bd0c1e236892 pippit-tool-cli-1.0.45-darwin-arm64.tar.gz
3
- 33095e1ceec2220ea1b79abfa56948dd5d237c1831ab2437c8af064d00db7c99 pippit-tool-cli-1.0.45-linux-amd64.tar.gz
4
- 85a91aeae7b9ea1324a8aee1be576fad12297cecdfe2828851147d50f4dbc177 pippit-tool-cli-1.0.45-linux-arm64.tar.gz
5
- e93240dbc814104ea278197aff983c2491089be1b9fe55f216007b1a612c7f93 pippit-tool-cli-1.0.45-windows-amd64.zip
6
- d5f196ea5b5f466911573a1177702ad00f27da153d2f29b5b1e65cd60f7f5100 pippit-tool-cli-1.0.45-windows-arm64.zip
1
+ 966d956f11b6922cd1112cc8e969578d7faaa093150676dc8fbcad1988ec185e pippit-tool-cli-1.0.47-darwin-amd64.tar.gz
2
+ 5bf74cd01214b76bc046c090e5dcdb8cccde9ac0af8ca1bcb9a01f45b7d7377f pippit-tool-cli-1.0.47-darwin-arm64.tar.gz
3
+ e043d77d2c7869b245081179abce175d9bd42608a3549205c143e7bc9aef76db pippit-tool-cli-1.0.47-linux-amd64.tar.gz
4
+ 3a1bd95631363c4702004da2b823ae34baf37576324eca5501d9e832ba8773cc pippit-tool-cli-1.0.47-linux-arm64.tar.gz
5
+ 3e0a59ff3f467ec1318c128d97fc33ec952b26715c4053c5abae27ee365f6f1d pippit-tool-cli-1.0.47-windows-amd64.zip
6
+ cbab4b7a34567a30b08abf2b2856958d0813aca6515b1a8edcead5939b47610c pippit-tool-cli-1.0.47-windows-arm64.zip