@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.
- package/README.md +23 -2
- package/checksums.txt +6 -6
- package/cmd/auth/auth.go +136 -142
- package/cmd/auth/auth_test.go +134 -0
- package/cmd/canvas/canvas.go +235 -0
- package/cmd/canvas/canvas_test.go +126 -0
- package/cmd/root.go +29 -4
- package/cmd/root_test.go +60 -0
- package/cmd/short_drama_test.go +4 -7
- package/cmd/update/update.go +34 -5
- package/cmd/update/update_test.go +84 -0
- package/internal/auth/auth_test.go +813 -0
- package/internal/auth/browser_darwin.go +14 -0
- package/internal/auth/browser_env.go +35 -0
- package/internal/auth/browser_linux.go +14 -0
- package/internal/auth/browser_windows.go +14 -0
- package/internal/auth/identity.go +89 -0
- package/internal/auth/loopback.go +324 -0
- package/internal/auth/manager.go +338 -144
- package/internal/auth/store.go +303 -0
- package/internal/auth/store_file_unix.go +176 -0
- package/internal/auth/store_file_windows.go +11 -0
- package/internal/auth/types.go +72 -0
- package/internal/canvas/allocate.go +70 -0
- package/internal/canvas/apply.go +203 -0
- package/internal/canvas/canvas_test.go +446 -0
- package/internal/canvas/create.go +380 -0
- package/internal/canvas/get.go +156 -0
- package/internal/canvas/types.go +68 -0
- package/internal/canvas/upload.go +250 -0
- package/internal/common/access_key.go +42 -6
- package/internal/common/access_key_test.go +113 -0
- package/internal/common/client.go +119 -21
- package/internal/common/client_test.go +213 -0
- package/internal/common/runner.go +15 -0
- package/internal/config/config.go +0 -20
- package/internal/config/config_test.go +0 -18
- package/package.json +1 -1
- package/skills/short-drama/SKILL.md +4 -4
- 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
|
@@ -86,14 +86,14 @@ metadata:
|
|
|
86
86
|
npx @pippit-dev/cli@latest install
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
Access Key 创建地址:https://xyq.jianying.com/home?tab_name=home
|
|
89
|
+
首次使用原生 CLI 时运行网页登录;CLI 会自动申请或复用本机专属凭证,并保存到系统安全凭证库,不要求用户复制 Access Key:
|
|
92
90
|
|
|
93
91
|
```bash
|
|
94
|
-
|
|
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
|
-
-
|
|
424
|
+
- 独立 Python 会话 API 脚本的鉴权方式为请求头 `Authorization: Bearer <XYQ_ACCESS_KEY>`
|
|
415
425
|
- 创建会话时 `message` 是用户的指令要求,不能为空
|
|
416
426
|
- 查询会话时可用 --after-seq 做增量拉取,便于轮询新消息(含 assistant 回复与生图/生视频结果)
|
|
417
427
|
- 上传文件仅支持图片(image/*)、视频(video/*)和 `.mp3/.wav` 音频文件,其他类型会被拒绝,文件大小须在 200MB 以下
|