@pippit-dev/cli 1.0.17 → 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 (40) hide show
  1. package/README.md +23 -2
  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/root.go +29 -4
  8. package/cmd/root_test.go +60 -0
  9. package/cmd/short_drama_test.go +4 -7
  10. package/cmd/update/update.go +34 -5
  11. package/cmd/update/update_test.go +84 -0
  12. package/internal/auth/auth_test.go +813 -0
  13. package/internal/auth/browser_darwin.go +14 -0
  14. package/internal/auth/browser_env.go +35 -0
  15. package/internal/auth/browser_linux.go +14 -0
  16. package/internal/auth/browser_windows.go +14 -0
  17. package/internal/auth/identity.go +89 -0
  18. package/internal/auth/loopback.go +324 -0
  19. package/internal/auth/manager.go +338 -144
  20. package/internal/auth/store.go +303 -0
  21. package/internal/auth/store_file_unix.go +176 -0
  22. package/internal/auth/store_file_windows.go +11 -0
  23. package/internal/auth/types.go +72 -0
  24. package/internal/canvas/allocate.go +70 -0
  25. package/internal/canvas/apply.go +203 -0
  26. package/internal/canvas/canvas_test.go +446 -0
  27. package/internal/canvas/create.go +380 -0
  28. package/internal/canvas/get.go +156 -0
  29. package/internal/canvas/types.go +68 -0
  30. package/internal/canvas/upload.go +250 -0
  31. package/internal/common/access_key.go +42 -6
  32. package/internal/common/access_key_test.go +113 -0
  33. package/internal/common/client.go +119 -21
  34. package/internal/common/client_test.go +213 -0
  35. package/internal/common/runner.go +15 -0
  36. package/internal/config/config.go +0 -20
  37. package/internal/config/config_test.go +0 -18
  38. package/package.json +1 -1
  39. package/skills/short-drama/SKILL.md +4 -4
  40. package/skills/xyq-nest-skill/SKILL.md +11 -1
@@ -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.17",
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 打开给用户,而不是只展示链接。
@@ -166,6 +166,16 @@ pippit-tool-cli query-result \
166
166
 
167
167
  ## 前置要求
168
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
+
169
179
  ```bash
170
180
  export XYQ_ACCESS_KEY="your-access-key"
171
181
  ```
@@ -411,7 +421,7 @@ python3 {baseDir}/scripts/download_results.py --urls URL1 URL2 URL3 --output-dir
411
421
 
412
422
  ## 注意事项
413
423
 
414
- - 鉴权方式为请求头 `Authorization: Bearer <XYQ_ACCESS_KEY>`
424
+ - 独立 Python 会话 API 脚本的鉴权方式为请求头 `Authorization: Bearer <XYQ_ACCESS_KEY>`
415
425
  - 创建会话时 `message` 是用户的指令要求,不能为空
416
426
  - 查询会话时可用 --after-seq 做增量拉取,便于轮询新消息(含 assistant 回复与生图/生视频结果)
417
427
  - 上传文件仅支持图片(image/*)、视频(video/*)和 `.mp3/.wav` 音频文件,其他类型会被拒绝,文件大小须在 200MB 以下