@pippit-dev/cli 1.0.16 → 1.0.18

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.
Files changed (41) hide show
  1. package/README.md +24 -3
  2. package/checksums.txt +6 -6
  3. package/cmd/auth/auth.go +136 -142
  4. package/cmd/auth/auth_test.go +134 -0
  5. package/cmd/canvas/canvas.go +235 -0
  6. package/cmd/canvas/canvas_test.go +126 -0
  7. package/cmd/generate_video/generate_video.go +1 -1
  8. package/cmd/root.go +29 -4
  9. package/cmd/root_test.go +60 -0
  10. package/cmd/short_drama_test.go +4 -7
  11. package/cmd/update/update.go +34 -5
  12. package/cmd/update/update_test.go +84 -0
  13. package/internal/auth/auth_test.go +813 -0
  14. package/internal/auth/browser_darwin.go +14 -0
  15. package/internal/auth/browser_env.go +35 -0
  16. package/internal/auth/browser_linux.go +14 -0
  17. package/internal/auth/browser_windows.go +14 -0
  18. package/internal/auth/identity.go +89 -0
  19. package/internal/auth/loopback.go +324 -0
  20. package/internal/auth/manager.go +338 -144
  21. package/internal/auth/store.go +303 -0
  22. package/internal/auth/store_file_unix.go +176 -0
  23. package/internal/auth/store_file_windows.go +11 -0
  24. package/internal/auth/types.go +72 -0
  25. package/internal/canvas/allocate.go +70 -0
  26. package/internal/canvas/apply.go +203 -0
  27. package/internal/canvas/canvas_test.go +446 -0
  28. package/internal/canvas/create.go +380 -0
  29. package/internal/canvas/get.go +156 -0
  30. package/internal/canvas/types.go +68 -0
  31. package/internal/canvas/upload.go +250 -0
  32. package/internal/common/access_key.go +42 -6
  33. package/internal/common/access_key_test.go +113 -0
  34. package/internal/common/client.go +119 -21
  35. package/internal/common/client_test.go +213 -0
  36. package/internal/common/runner.go +15 -0
  37. package/internal/config/config.go +0 -20
  38. package/internal/config/config_test.go +0 -18
  39. package/package.json +1 -1
  40. package/skills/short-drama/SKILL.md +4 -4
  41. package/skills/xyq-nest-skill/SKILL.md +17 -6
@@ -0,0 +1,213 @@
1
+ package common
2
+
3
+ import (
4
+ "context"
5
+ "net/http"
6
+ "net/http/httptest"
7
+ "net/url"
8
+ "os"
9
+ "path/filepath"
10
+ "strings"
11
+ "sync/atomic"
12
+ "testing"
13
+ "time"
14
+ )
15
+
16
+ type observedHeaders struct {
17
+ authorization string
18
+ }
19
+
20
+ func TestHTTPClientScopesCredentialsToBaseOrigin(t *testing.T) {
21
+ apiHeaders := make(chan observedHeaders, 1)
22
+ api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23
+ apiHeaders <- captureRoutingHeaders(r)
24
+ _, _ = w.Write([]byte(`{}`))
25
+ }))
26
+ defer api.Close()
27
+
28
+ thirdPartyHeaders := make(chan observedHeaders, 1)
29
+ thirdParty := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
30
+ thirdPartyHeaders <- captureRoutingHeaders(r)
31
+ _, _ = w.Write([]byte(`{}`))
32
+ }))
33
+ defer thirdParty.Close()
34
+
35
+ client := NewHTTPClient(
36
+ api.URL,
37
+ time.Second,
38
+ NewAccessKeyAuthorizer("secret-ak"),
39
+ )
40
+ if err := client.SendRequest(context.Background(), "/api/test", nil, nil); err != nil {
41
+ t.Fatalf("same-origin SendRequest() error = %v", err)
42
+ }
43
+ gotAPI := <-apiHeaders
44
+ if gotAPI.authorization != "Bearer secret-ak" {
45
+ t.Fatalf("same-origin Authorization = %q", gotAPI.authorization)
46
+ }
47
+
48
+ thirdPartyURL := thirdParty.URL + "/asset.mp4"
49
+ err := client.SendRequestWithHeaders(context.Background(), thirdPartyURL, nil, map[string]string{
50
+ "Authorization": "Bearer caller-supplied",
51
+ }, nil)
52
+ if err != nil {
53
+ t.Fatalf("third-party SendRequest() error = %v", err)
54
+ }
55
+ gotThirdParty := <-thirdPartyHeaders
56
+ if gotThirdParty != (observedHeaders{}) {
57
+ t.Fatalf("third-party sensitive headers leaked: %#v", gotThirdParty)
58
+ }
59
+ }
60
+
61
+ func TestHTTPClientTreatsSameOriginAbsoluteURLAsAPI(t *testing.T) {
62
+ headers := make(chan observedHeaders, 1)
63
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
64
+ headers <- captureRoutingHeaders(r)
65
+ _, _ = w.Write([]byte(`{}`))
66
+ }))
67
+ defer server.Close()
68
+
69
+ client := NewHTTPClient(
70
+ server.URL+"/base-path",
71
+ time.Second,
72
+ NewAccessKeyAuthorizer("same-origin-ak"),
73
+ )
74
+ if err := client.SendRequest(context.Background(), server.URL+"/api/absolute", nil, nil); err != nil {
75
+ t.Fatalf("SendRequest() error = %v", err)
76
+ }
77
+ got := <-headers
78
+ if got.authorization != "Bearer same-origin-ak" {
79
+ t.Fatalf("same-origin absolute headers = %#v", got)
80
+ }
81
+ }
82
+
83
+ func TestHTTPClientRejectsAPICrossOriginRedirectBeforeSendingBody(t *testing.T) {
84
+ var thirdPartyRequests atomic.Int32
85
+ thirdParty := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
86
+ thirdPartyRequests.Add(1)
87
+ _, _ = w.Write([]byte(`{}`))
88
+ }))
89
+ defer thirdParty.Close()
90
+
91
+ api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
92
+ http.Redirect(w, r, thirdParty.URL+"/redirected-asset", http.StatusTemporaryRedirect)
93
+ }))
94
+ defer api.Close()
95
+
96
+ client := NewHTTPClient(
97
+ api.URL,
98
+ time.Second,
99
+ NewAccessKeyAuthorizer("redirect-ak"),
100
+ )
101
+ err := client.SendRequest(context.Background(), "/api/redirect", map[string]string{"canvas": "private"}, nil)
102
+ if err == nil || !strings.Contains(err.Error(), "拒绝 Pippit API 跨域重定向") {
103
+ t.Fatalf("SendRequest() error = %v, want cross-origin redirect rejection", err)
104
+ }
105
+ if got := thirdPartyRequests.Load(); got != 0 {
106
+ t.Fatalf("third party received %d API redirect requests, want 0", got)
107
+ }
108
+ }
109
+
110
+ func TestHTTPClientDoesNotRestoreAuthorizationAfterCrossOriginRedirect(t *testing.T) {
111
+ finalHeaders := make(chan observedHeaders, 1)
112
+ var apiURL string
113
+ thirdParty := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
114
+ if got := captureRoutingHeaders(r); got != (observedHeaders{}) {
115
+ t.Errorf("cross-origin hop leaked protected headers: %#v", got)
116
+ }
117
+ http.Redirect(w, r, apiURL+"/final", http.StatusFound)
118
+ }))
119
+ defer thirdParty.Close()
120
+
121
+ api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
122
+ finalHeaders <- captureRoutingHeaders(r)
123
+ _, _ = w.Write([]byte(`{}`))
124
+ }))
125
+ defer api.Close()
126
+ apiURL = api.URL
127
+
128
+ client := NewHTTPClient(
129
+ api.URL,
130
+ time.Second,
131
+ NewAccessKeyAuthorizer("redirect-bounce-ak"),
132
+ )
133
+ if err := client.SendRequest(context.Background(), thirdParty.URL+"/bounce", nil, nil); err != nil {
134
+ t.Fatalf("SendRequest() error = %v", err)
135
+ }
136
+ if got := <-finalHeaders; got != (observedHeaders{}) {
137
+ t.Fatalf("trusted return hop restored protected headers: %#v", got)
138
+ }
139
+ }
140
+
141
+ func TestHTTPClientPreventsCallerAuthorizationOverride(t *testing.T) {
142
+ headers := make(chan observedHeaders, 1)
143
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
144
+ headers <- captureRoutingHeaders(r)
145
+ _, _ = w.Write([]byte(`{}`))
146
+ }))
147
+ defer server.Close()
148
+
149
+ client := NewHTTPClient(
150
+ server.URL,
151
+ time.Second,
152
+ NewAccessKeyAuthorizer("production-ak"),
153
+ )
154
+ err := client.SendRequestWithHeaders(context.Background(), "/api/test", nil, map[string]string{
155
+ "Authorization": "Bearer caller-override",
156
+ }, nil)
157
+ if err != nil {
158
+ t.Fatalf("SendRequest() error = %v", err)
159
+ }
160
+ got := <-headers
161
+ if got.authorization != "Bearer production-ak" {
162
+ t.Fatalf("protected headers = %#v", got)
163
+ }
164
+ }
165
+
166
+ func TestHTTPClientMultipartRequestInjectsOnlyAuthorization(t *testing.T) {
167
+ headers := make(chan observedHeaders, 1)
168
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
169
+ headers <- captureRoutingHeaders(r)
170
+ _, _ = w.Write([]byte(`{}`))
171
+ }))
172
+ defer server.Close()
173
+
174
+ filePath := filepath.Join(t.TempDir(), "sample.txt")
175
+ if err := os.WriteFile(filePath, []byte("sample"), 0o600); err != nil {
176
+ t.Fatal(err)
177
+ }
178
+ client := NewHTTPClient(
179
+ server.URL,
180
+ time.Second,
181
+ NewAccessKeyAuthorizer("upload-ak"),
182
+ )
183
+ err := client.SendMultipartRequest(context.Background(), "/api/upload", nil, MultipartFile{
184
+ FieldName: "file",
185
+ Path: filePath,
186
+ }, nil)
187
+ if err != nil {
188
+ t.Fatalf("SendMultipartRequest() error = %v", err)
189
+ }
190
+ got := <-headers
191
+ if got.authorization != "Bearer upload-ak" {
192
+ t.Fatalf("multipart headers = %#v", got)
193
+ }
194
+ }
195
+
196
+ func TestSameOriginUsesEffectiveDefaultPorts(t *testing.T) {
197
+ httpsDefault, _ := url.Parse("https://xyq.jianying.com/api")
198
+ httpsExplicit, _ := url.Parse("https://XYQ.JIANYING.COM:443/asset")
199
+ if !sameOrigin(httpsDefault, httpsExplicit) {
200
+ t.Fatal("sameOrigin() = false for equivalent HTTPS origins")
201
+ }
202
+
203
+ differentPort, _ := url.Parse("https://xyq.jianying.com:8443/api")
204
+ if sameOrigin(httpsDefault, differentPort) {
205
+ t.Fatal("sameOrigin() = true for different ports")
206
+ }
207
+ }
208
+
209
+ func captureRoutingHeaders(r *http.Request) observedHeaders {
210
+ return observedHeaders{
211
+ authorization: r.Header.Get("Authorization"),
212
+ }
213
+ }
@@ -1,13 +1,28 @@
1
1
  package common
2
2
 
3
3
  import (
4
+ "context"
5
+
6
+ "github.com/Pippit-dev/pippit-cli/internal/auth"
4
7
  "github.com/Pippit-dev/pippit-cli/internal/config"
5
8
  )
6
9
 
10
+ // AuthManager is the credential boundary shared by native CLI commands. It
11
+ // resolves explicit CI credentials first and browser-managed credentials
12
+ // second, without exposing either value to command output.
13
+ type AuthManager interface {
14
+ ResolveAccessKey(context.Context) (string, error)
15
+ Login(context.Context, auth.LoginOptions) (*auth.Credential, error)
16
+ Status(context.Context) (*auth.Status, error)
17
+ Logout(context.Context, bool) error
18
+ CredentialScope(context.Context) (string, error)
19
+ }
20
+
7
21
  // Runner carries runtime dependencies for command execution.
8
22
  type Runner struct {
9
23
  Config *config.Config
10
24
  Client Client
25
+ Auth AuthManager
11
26
  }
12
27
 
13
28
  func NewRunner(cfg *config.Config, client Client) *Runner {
@@ -10,8 +10,6 @@ const (
10
10
  DefaultBaseURL = "https://xyq.jianying.com"
11
11
  DefaultHTTPTimeout = 30 * time.Minute
12
12
  DefaultAuthTTL = 30 * time.Second
13
- DefaultOAuthClientKey = "mock-cli"
14
- DefaultOAuthBaseURL = "https://passport.bytedance.com"
15
13
  DefaultAuthStoreServiceName = "pippit-cli"
16
14
  SubmitRunPath = "/api/biz/v1/skill/submit_run"
17
15
  GetThreadPath = "/api/biz/v1/skill/get_thread"
@@ -27,17 +25,9 @@ type Config struct {
27
25
  HTTPTimeout time.Duration
28
26
  AuthTTL time.Duration
29
27
  AccessKey string
30
- OAuth *OAuth
31
28
  Paths *Paths
32
29
  }
33
30
 
34
- type OAuth struct {
35
- ClientKey string
36
- BaseURL string
37
- StoreServiceName string
38
- Scopes []string
39
- }
40
-
41
31
  type Paths struct {
42
32
  SubmitRun string
43
33
  GetThread string
@@ -52,7 +42,6 @@ func Load() *Config {
52
42
  HTTPTimeout: DefaultHTTPTimeout,
53
43
  AuthTTL: DefaultAuthTTL,
54
44
  AccessKey: strings.TrimSpace(os.Getenv(EnvXYQAccessKey)),
55
- OAuth: resolveOAuth(),
56
45
  Paths: &Paths{
57
46
  SubmitRun: SubmitRunPath,
58
47
  GetThread: GetThreadPath,
@@ -61,12 +50,3 @@ func Load() *Config {
61
50
  },
62
51
  }
63
52
  }
64
-
65
- func resolveOAuth() *OAuth {
66
- return &OAuth{
67
- ClientKey: DefaultOAuthClientKey,
68
- BaseURL: DefaultOAuthBaseURL,
69
- StoreServiceName: DefaultAuthStoreServiceName,
70
- Scopes: []string{"user_info", "aigc_generate"},
71
- }
72
- }
@@ -17,24 +17,6 @@ func TestLoadUsesDefaultConfig(t *testing.T) {
17
17
  if cfg.AccessKey != "" {
18
18
  t.Fatalf("AccessKey = %q, want empty", cfg.AccessKey)
19
19
  }
20
- if cfg.OAuth.ClientKey != DefaultOAuthClientKey {
21
- t.Fatalf("OAuth.ClientKey = %q, want %q", cfg.OAuth.ClientKey, DefaultOAuthClientKey)
22
- }
23
- if cfg.OAuth.StoreServiceName != DefaultAuthStoreServiceName {
24
- t.Fatalf("OAuth.StoreServiceName = %q, want %q", cfg.OAuth.StoreServiceName, DefaultAuthStoreServiceName)
25
- }
26
- if cfg.OAuth.BaseURL != DefaultOAuthBaseURL {
27
- t.Fatalf("OAuth.BaseURL = %q, want %q", cfg.OAuth.BaseURL, DefaultOAuthBaseURL)
28
- }
29
- wantScopes := []string{"user_info", "aigc_generate"}
30
- if len(cfg.OAuth.Scopes) != len(wantScopes) {
31
- t.Fatalf("OAuth.Scopes = %#v, want %#v", cfg.OAuth.Scopes, wantScopes)
32
- }
33
- for i := range wantScopes {
34
- if cfg.OAuth.Scopes[i] != wantScopes[i] {
35
- t.Fatalf("OAuth.Scopes = %#v, want %#v", cfg.OAuth.Scopes, wantScopes)
36
- }
37
- }
38
20
  if cfg.Paths.SubmitRun != SubmitRunPath {
39
21
  t.Fatalf("SubmitRun path = %q, want %q", cfg.Paths.SubmitRun, SubmitRunPath)
40
22
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pippit-dev/cli",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "Pippit CLI",
5
5
  "bin": {
6
6
  "pippit-tool-cli": "scripts/run.js"
@@ -86,14 +86,14 @@ metadata:
86
86
  npx @pippit-dev/cli@latest install
87
87
  ```
88
88
 
89
- 部分功能需要先配置 `XYQ_ACCESS_KEY`。缺失时 CLI 会直接提示用户先创建 Access Key,此时请等待用户给与Access Key后再继续运行。
90
-
91
- Access Key 创建地址:https://xyq.jianying.com/home?tab_name=home
89
+ 首次使用原生 CLI 时运行网页登录;CLI 会自动申请或复用本机专属凭证,并保存到系统安全凭证库,不要求用户复制 Access Key
92
90
 
93
91
  ```bash
94
- export XYQ_ACCESS_KEY="<access-key>"
92
+ pippit-tool-cli login
95
93
  ```
96
94
 
95
+ `XYQ_ACCESS_KEY` 仅保留给 CI、Agent 等非交互环境作为显式覆盖。若该环境变量已设置但无效,CLI 不会静默改用个人网页登录凭证;应先修正或取消该环境变量。
96
+
97
97
  ## 小云雀界面打开契约
98
98
 
99
99
  `+submit-run` 返回 `web_thread_link` 后,用户侧 Agent 必须优先把小云雀短剧 WebUI 打开给用户,而不是只展示链接。
@@ -71,7 +71,7 @@ pippit-tool-cli query-result \
71
71
  满足任一条件时,必须直接使用 `pippit-tool-cli generate-video`,不要改走 `submit_run.py`:
72
72
 
73
73
  - 用户明确说“视频模型直出”、“直接调模型”或“直接调用 CLI”。
74
- - 用户指定了具体视频模型(如 Seedance),并希望单次直接生成视频。
74
+ - 用户指定了具体视频模型(如 `Seedance_2.5`),并希望单次直接生成视频。
75
75
  - 用户明确要求“首尾帧生视频”、指定首帧和尾帧,或要求从第一张图过渡到第二张图。
76
76
  - 上游流程已明确将任务标记为 direct-model / 模型直出。
77
77
 
@@ -81,12 +81,13 @@ pippit-tool-cli query-result \
81
81
  2. 真实提交会消耗 credits;如果用户本轮尚未明确确认生成,按“用户确认与反问”规则征得明确确认后再运行。
82
82
  3. 保留用户原始 prompt,不要自行扩写、润色、翻译或增加风格词。
83
83
  4. 只添加用户已经给出的 `--model`、`--duration`、`--ratio`、`--resolution`、`--image`、`--video`、`--audio`、`--generate-type` 参数;未给参数交给 CLI 默认值。
84
- 5. 首尾帧请求固定传 `--generate-type 1`,并按首帧、尾帧顺序传入两次 `--image`,不得重排。用户未明确两张图片的角色或缺少任一张时,先询问用户;不要在 skill 侧维护额外的 `generate_type` 枚举 allowlist,其他值原样交给服务端处理。
85
- 6. `generate-video` 返回后,保存 `thread_id`、`run_id`,并立即向用户展示 `web_thread_link`。
86
- 7. 每隔 10 秒调用 `query-result`,直到 `completed=true`。出现 `error_message` 时停止并报告;成功时展示并下载 `videos[].output_path`。
84
+ 5. 普通用户支持模型 `Seedance_2.0_mini_lite`;VIP 专属模型包括 `seedance2.0_vision`、`seedance2.0_fast_vision`、`Seedance_2.0_mini` `Seedance_2.5`。该列表仅用于指导用户选择和传入准确的 `--model` 值,不要在 skill 侧增加模型枚举校验,实际合法性由服务端决定。
85
+ 6. 首尾帧请求固定传 `--generate-type 1`,并按首帧、尾帧顺序传入两次 `--image`,不得重排。用户未明确两张图片的角色或缺少任一张时,先询问用户;不要在 skill 侧维护额外的 `generate_type` 枚举 allowlist,其他值原样交给服务端处理。
86
+ 7. `generate-video` 返回后,保存 `thread_id`、`run_id`,并立即向用户展示 `web_thread_link`。
87
+ 8. 每隔 10 秒调用 `query-result`,直到 `completed=true`。出现 `error_message` 时停止并报告;成功时展示并下载 `videos[].output_path`。
87
88
 
88
89
  ```bash
89
- pippit-tool-cli generate-video --prompt "用户原始描述"
90
+ pippit-tool-cli generate-video --prompt "用户原始描述" --model "Seedance_2.5"
90
91
 
91
92
  pippit-tool-cli generate-video \
92
93
  --prompt "用户原始描述" \
@@ -165,6 +166,16 @@ pippit-tool-cli query-result \
165
166
 
166
167
  ## 前置要求
167
168
 
169
+ 图片/视频模型直出和视频处理(路由 A/B/C)使用原生 CLI。首次使用时运行网页登录,CLI 会自动申请或复用本机专属凭证,并保存到系统安全凭证库:
170
+
171
+ ```bash
172
+ pippit-tool-cli login
173
+ ```
174
+
175
+ `XYQ_ACCESS_KEY` 仅作为原生 CLI 在 CI、Agent 等非交互环境中的显式覆盖。如果该环境变量已经设置但无效,CLI 不会静默改用网页登录凭证,应先修正或取消该环境变量。
176
+
177
+ 默认的后端 Agent 编排(路由 D)仍由独立 Python 脚本 `submit_run.py`、`get_thread.py` 和 `upload_file.py` 执行。这些脚本尚未接入 CLI 的系统安全凭证库,使用前必须配置:
178
+
168
179
  ```bash
169
180
  export XYQ_ACCESS_KEY="your-access-key"
170
181
  ```
@@ -410,7 +421,7 @@ python3 {baseDir}/scripts/download_results.py --urls URL1 URL2 URL3 --output-dir
410
421
 
411
422
  ## 注意事项
412
423
 
413
- - 鉴权方式为请求头 `Authorization: Bearer <XYQ_ACCESS_KEY>`
424
+ - 独立 Python 会话 API 脚本的鉴权方式为请求头 `Authorization: Bearer <XYQ_ACCESS_KEY>`
414
425
  - 创建会话时 `message` 是用户的指令要求,不能为空
415
426
  - 查询会话时可用 --after-seq 做增量拉取,便于轮询新消息(含 assistant 回复与生图/生视频结果)
416
427
  - 上传文件仅支持图片(image/*)、视频(video/*)和 `.mp3/.wav` 音频文件,其他类型会被拒绝,文件大小须在 200MB 以下