@pippit-dev/cli 1.0.30 → 1.0.32
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 +1 -1
- package/checksums.txt +6 -6
- package/cmd/marketing.go +144 -0
- package/cmd/marketing_test.go +179 -0
- package/cmd/root.go +1 -0
- package/cmd/update/update.go +2 -1
- package/cmd/update/update_test.go +3 -2
- package/dist/checksums.txt +6 -6
- package/internal/common/client.go +11 -1
- package/package.json +1 -1
- package/scripts/skills.js +2 -1
- package/skills/xyq-marketing-skill/SKILL.md +8 -6
- package/skills/xyq-marketing-skill/examples/product-video.md +1 -1
- package/skills/xyq-marketing-skill/references/api.md +2 -2
- package/skills/xyq-marketing-skill/scripts/marketing.js +76 -38
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
npx skills add Pippit-dev/cli --skill xyq-marketing-skill
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
需要 Node.js 16
|
|
31
|
+
需要 Node.js 16+ 和支持 `marketing` 命令的 CLI。Skill 脚本通过原生 `marketing` 命令复用 CLI 登录态;先运行 `pippit-tool-cli status`,未登录时执行 `pippit-tool-cli login` 完成浏览器授权,无需用户提供 access_token 或 Access Key。生成请求通过 stdin 传入原生 CLI,凭据仅在 CLI 内用于鉴权。
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
34
|
# 从仓库根目录执行;营销请求字段见接口契约,默认只预览
|
package/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
2f3c2a486a66e408a6d3841f19ecb909fbaa657636a469ee782a424e59596a68 pippit-tool-cli-1.0.32-darwin-amd64.tar.gz
|
|
2
|
+
f4ccd97a007167a9677ea85be52339a041f5448c4e4afd2ec996d32006ccec83 pippit-tool-cli-1.0.32-darwin-arm64.tar.gz
|
|
3
|
+
91ae74eb2067844be741aa8a5f5f932152214c168d43efae801244d46e3f5cab pippit-tool-cli-1.0.32-linux-amd64.tar.gz
|
|
4
|
+
1928d81ea0f11fa20ae65a689654a9d2dddc4cacb4e6a5ae41a5cdd35e40eac5 pippit-tool-cli-1.0.32-linux-arm64.tar.gz
|
|
5
|
+
79283336e84c3196454aecf96c367383795636f4e153234a11996124a78dd7a5 pippit-tool-cli-1.0.32-windows-amd64.zip
|
|
6
|
+
aa73d963a3dd7d0eb5dba433fdf14d97510dfbe90515aab5318e55c0147d14d9 pippit-tool-cli-1.0.32-windows-arm64.zip
|
package/cmd/marketing.go
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"io"
|
|
8
|
+
"mime"
|
|
9
|
+
"os"
|
|
10
|
+
"path/filepath"
|
|
11
|
+
"strings"
|
|
12
|
+
"time"
|
|
13
|
+
|
|
14
|
+
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
15
|
+
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
16
|
+
"github.com/spf13/cobra"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
var marketingPaths = map[string]string{
|
|
20
|
+
"generate": "/api/biz/v1/agent/submit_marketing_run",
|
|
21
|
+
"query": "/api/biz/v1/agent/query_generate_video_result",
|
|
22
|
+
"upload": config.UploadFilePath,
|
|
23
|
+
"balance": config.GetCreditBalancePath,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Marketing uses the same AuthManager as login/status and all other commands.
|
|
27
|
+
// Only fixed public endpoints are exposed; credentials never leave the CLI.
|
|
28
|
+
func newMarketingCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
|
|
29
|
+
root := &cobra.Command{Use: "marketing", Short: "Marketing API using the shared CLI login"}
|
|
30
|
+
root.SetOut(stdout)
|
|
31
|
+
root.SetErr(stderr)
|
|
32
|
+
for _, action := range []string{"generate", "query", "upload", "balance"} {
|
|
33
|
+
root.AddCommand(newMarketingAction(action, stdout, stderr, runner))
|
|
34
|
+
}
|
|
35
|
+
return root
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func newMarketingAction(action string, stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
|
|
39
|
+
var requestFile, file, threadID, runID string
|
|
40
|
+
var execute bool
|
|
41
|
+
var timeout time.Duration
|
|
42
|
+
command := &cobra.Command{Use: action, Args: cobra.NoArgs, Short: "Call marketing " + action}
|
|
43
|
+
command.SetOut(stdout)
|
|
44
|
+
command.SetErr(stderr)
|
|
45
|
+
command.Flags().DurationVar(&timeout, "timeout", 60*time.Second, "request deadline (e.g. 60s)")
|
|
46
|
+
switch action {
|
|
47
|
+
case "generate":
|
|
48
|
+
command.Flags().StringVar(&requestFile, "request", "", "request JSON file, or - for stdin")
|
|
49
|
+
command.Flags().BoolVar(&execute, "execute", false, "submit generation; otherwise preview only")
|
|
50
|
+
case "query":
|
|
51
|
+
command.Flags().StringVar(&threadID, "thread-id", "", "marketing thread ID")
|
|
52
|
+
command.Flags().StringVar(&runID, "run-id", "", "marketing run ID")
|
|
53
|
+
case "upload":
|
|
54
|
+
command.Flags().StringVar(&file, "file", "", "local media file")
|
|
55
|
+
}
|
|
56
|
+
command.RunE = func(cmd *cobra.Command, _ []string) error {
|
|
57
|
+
if timeout <= 0 || timeout > 30*time.Minute {
|
|
58
|
+
return fmt.Errorf("timeout 必须大于 0 且不超过 30m")
|
|
59
|
+
}
|
|
60
|
+
var body any = map[string]any{}
|
|
61
|
+
switch action {
|
|
62
|
+
case "generate":
|
|
63
|
+
if requestFile == "" {
|
|
64
|
+
return fmt.Errorf("缺少必填参数 --request")
|
|
65
|
+
}
|
|
66
|
+
reader := cmd.InOrStdin()
|
|
67
|
+
if requestFile != "-" {
|
|
68
|
+
f, err := os.Open(requestFile)
|
|
69
|
+
if err != nil {
|
|
70
|
+
return err
|
|
71
|
+
}
|
|
72
|
+
defer f.Close()
|
|
73
|
+
reader = f
|
|
74
|
+
}
|
|
75
|
+
var value map[string]json.RawMessage
|
|
76
|
+
decoder := json.NewDecoder(io.LimitReader(reader, 8*1024*1024+1))
|
|
77
|
+
if err := decoder.Decode(&value); err != nil {
|
|
78
|
+
return fmt.Errorf("请求 JSON 无效: %w", err)
|
|
79
|
+
}
|
|
80
|
+
var extra any
|
|
81
|
+
if err := decoder.Decode(&extra); err != io.EOF {
|
|
82
|
+
return fmt.Errorf("请求只能包含一个 JSON 对象")
|
|
83
|
+
}
|
|
84
|
+
for key := range value {
|
|
85
|
+
if key != "message" && key != "asset_ids" && key != "thread_id" && key != "general_agent_settings" {
|
|
86
|
+
return fmt.Errorf("未支持的营销请求字段: %s", key)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
var message string
|
|
90
|
+
var settings struct {
|
|
91
|
+
VideoModel string `json:"video_model"`
|
|
92
|
+
}
|
|
93
|
+
if json.Unmarshal(value["message"], &message) != nil || strings.TrimSpace(message) == "" {
|
|
94
|
+
return fmt.Errorf("message 必须为非空字符串")
|
|
95
|
+
}
|
|
96
|
+
if json.Unmarshal(value["general_agent_settings"], &settings) != nil || strings.TrimSpace(settings.VideoModel) == "" {
|
|
97
|
+
return fmt.Errorf("general_agent_settings.video_model 必填")
|
|
98
|
+
}
|
|
99
|
+
body = value
|
|
100
|
+
if !execute {
|
|
101
|
+
return common.WriteJSON(stdout, map[string]any{"dry_run": true, "url": config.DefaultBaseURL + marketingPaths[action], "body": body})
|
|
102
|
+
}
|
|
103
|
+
case "query":
|
|
104
|
+
if strings.TrimSpace(threadID) == "" || strings.TrimSpace(runID) == "" {
|
|
105
|
+
return fmt.Errorf("query 需要 --thread-id 和 --run-id")
|
|
106
|
+
}
|
|
107
|
+
body = map[string]string{"thread_id": threadID, "run_id": runID}
|
|
108
|
+
case "upload":
|
|
109
|
+
if err := validateMediaUpload(file); err != nil {
|
|
110
|
+
return err
|
|
111
|
+
}
|
|
112
|
+
info, err := os.Stat(file)
|
|
113
|
+
if err != nil {
|
|
114
|
+
return err
|
|
115
|
+
}
|
|
116
|
+
if info.Size() == 0 {
|
|
117
|
+
return fmt.Errorf("上传需要非空文件")
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
ctx, cancel := context.WithTimeout(cmd.Context(), timeout)
|
|
121
|
+
defer cancel()
|
|
122
|
+
client := common.NewNonRedirectingHTTPClient(runner.Config.BaseURL, timeout, newRunnerAuthorizer(runner))
|
|
123
|
+
var result map[string]json.RawMessage
|
|
124
|
+
var err error
|
|
125
|
+
if action == "upload" {
|
|
126
|
+
contentType := mime.TypeByExtension(strings.ToLower(filepath.Ext(file)))
|
|
127
|
+
err = client.SendMultipartRequest(ctx, marketingPaths[action], nil, common.MultipartFile{FieldName: "file", Path: file, ContentType: contentType}, &result)
|
|
128
|
+
} else {
|
|
129
|
+
err = client.SendRequest(ctx, marketingPaths[action], body, &result)
|
|
130
|
+
}
|
|
131
|
+
if err != nil {
|
|
132
|
+
return err
|
|
133
|
+
}
|
|
134
|
+
ret := strings.TrimSpace(string(result["ret"]))
|
|
135
|
+
if ret != `"0"` && ret != "0" {
|
|
136
|
+
var message, logID string
|
|
137
|
+
_ = json.Unmarshal(result["errmsg"], &message)
|
|
138
|
+
_ = json.Unmarshal(result["log_id"], &logID)
|
|
139
|
+
return common.NewLogIDError(fmt.Sprintf("营销 API 失败: ret=%s errmsg=%s", ret, message), logID)
|
|
140
|
+
}
|
|
141
|
+
return common.WriteJSON(stdout, result)
|
|
142
|
+
}
|
|
143
|
+
return command
|
|
144
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"fmt"
|
|
7
|
+
"io"
|
|
8
|
+
"net/http"
|
|
9
|
+
"net/http/httptest"
|
|
10
|
+
"os"
|
|
11
|
+
"path/filepath"
|
|
12
|
+
"strings"
|
|
13
|
+
"testing"
|
|
14
|
+
"time"
|
|
15
|
+
|
|
16
|
+
"github.com/Pippit-dev/pippit-cli/internal/auth"
|
|
17
|
+
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
type marketingCredentialStore struct {
|
|
21
|
+
auth.CredentialStore
|
|
22
|
+
credential *auth.Credential
|
|
23
|
+
loads int
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func (s *marketingCredentialStore) Load(context.Context) (*auth.Credential, error) {
|
|
27
|
+
s.loads++
|
|
28
|
+
if s.credential == nil {
|
|
29
|
+
return nil, auth.ErrCredentialNotFound
|
|
30
|
+
}
|
|
31
|
+
return s.credential, nil
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const marketingRequest = `{"message":"make an ad","general_agent_settings":{"video_model":"chosen-model","show_subtitle":false}}`
|
|
35
|
+
|
|
36
|
+
func TestMarketingUsesSharedBrowserAuth(t *testing.T) {
|
|
37
|
+
for _, action := range []string{"generate", "query", "upload", "balance"} {
|
|
38
|
+
t.Run(action, func(t *testing.T) {
|
|
39
|
+
calls := 0
|
|
40
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
41
|
+
calls++
|
|
42
|
+
if r.Header.Get("Authorization") != "Bearer browser-secret" {
|
|
43
|
+
t.Error("browser login not reused")
|
|
44
|
+
}
|
|
45
|
+
if r.URL.Path != marketingPaths[action] || r.Method != "POST" {
|
|
46
|
+
t.Error("wrong endpoint")
|
|
47
|
+
}
|
|
48
|
+
data, _ := io.ReadAll(r.Body)
|
|
49
|
+
if bytes.Contains(data, []byte("browser-secret")) {
|
|
50
|
+
t.Error("credential leaked into payload")
|
|
51
|
+
}
|
|
52
|
+
if action == "upload" && !bytes.Contains(data, []byte(`name="file"`)) {
|
|
53
|
+
t.Error("missing multipart file")
|
|
54
|
+
}
|
|
55
|
+
fmt.Fprint(w, `{"ret":"0","log_id":"log-test","data":{"thread_id":"thread","run_id":"run"}}`)
|
|
56
|
+
}))
|
|
57
|
+
defer server.Close()
|
|
58
|
+
cfg := config.Load()
|
|
59
|
+
cfg.BaseURL = server.URL
|
|
60
|
+
cfg.AccessKey = ""
|
|
61
|
+
store := &marketingCredentialStore{credential: &auth.Credential{AccessKey: "browser-secret", UID: "user", DeviceID: "device", ExpiredAt: time.Now().Add(time.Hour).Unix()}}
|
|
62
|
+
runner := newRootRunner(cfg)
|
|
63
|
+
runner.Auth = auth.NewManager(cfg, auth.WithCredentialStore(store))
|
|
64
|
+
var output bytes.Buffer
|
|
65
|
+
root := newRootCommand(&output, io.Discard, runner)
|
|
66
|
+
args := []string{"marketing", action}
|
|
67
|
+
switch action {
|
|
68
|
+
case "generate":
|
|
69
|
+
args = append(args, "--request", "-", "--execute")
|
|
70
|
+
root.SetIn(strings.NewReader(marketingRequest))
|
|
71
|
+
case "query":
|
|
72
|
+
args = append(args, "--thread-id", "thread", "--run-id", "run")
|
|
73
|
+
case "upload":
|
|
74
|
+
file := filepath.Join(t.TempDir(), "product.png")
|
|
75
|
+
if err := os.WriteFile(file, []byte("image"), 0600); err != nil {
|
|
76
|
+
t.Fatal(err)
|
|
77
|
+
}
|
|
78
|
+
args = append(args, "--file", file)
|
|
79
|
+
}
|
|
80
|
+
root.SetArgs(args)
|
|
81
|
+
if err := root.Execute(); err != nil {
|
|
82
|
+
t.Fatal(err)
|
|
83
|
+
}
|
|
84
|
+
if calls != 1 || store.loads != 1 {
|
|
85
|
+
t.Fatalf("HTTP calls=%d credential loads=%d", calls, store.loads)
|
|
86
|
+
}
|
|
87
|
+
if strings.Contains(output.String(), "browser-secret") || !strings.Contains(output.String(), "log-test") {
|
|
88
|
+
t.Fatal("raw result or credential boundary broken")
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func TestMarketingAuthFailureAndEnvironmentPrecedence(t *testing.T) {
|
|
95
|
+
for _, scenario := range []string{"missing", "expired", "override"} {
|
|
96
|
+
t.Run(scenario, func(t *testing.T) {
|
|
97
|
+
calls := 0
|
|
98
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
99
|
+
calls++
|
|
100
|
+
if r.Header.Get("Authorization") != "Bearer explicit-key" {
|
|
101
|
+
t.Error("environment override not preserved")
|
|
102
|
+
}
|
|
103
|
+
fmt.Fprint(w, `{"ret":"0","data":{}}`)
|
|
104
|
+
}))
|
|
105
|
+
defer server.Close()
|
|
106
|
+
cfg := config.Load()
|
|
107
|
+
cfg.BaseURL = server.URL
|
|
108
|
+
cfg.AccessKey = ""
|
|
109
|
+
store := &marketingCredentialStore{}
|
|
110
|
+
if scenario == "expired" {
|
|
111
|
+
store.credential = &auth.Credential{AccessKey: "expired", ExpiredAt: 1}
|
|
112
|
+
}
|
|
113
|
+
if scenario == "override" {
|
|
114
|
+
cfg.AccessKey = "explicit-key"
|
|
115
|
+
}
|
|
116
|
+
runner := newRootRunner(cfg)
|
|
117
|
+
runner.Auth = auth.NewManager(cfg, auth.WithCredentialStore(store))
|
|
118
|
+
root := newRootCommand(io.Discard, io.Discard, runner)
|
|
119
|
+
root.SetArgs([]string{"marketing", "balance"})
|
|
120
|
+
err := root.Execute()
|
|
121
|
+
if scenario == "override" {
|
|
122
|
+
if err != nil || calls != 1 || store.loads != 0 {
|
|
123
|
+
t.Fatalf("override: err=%v calls=%d loads=%d", err, calls, store.loads)
|
|
124
|
+
}
|
|
125
|
+
} else if err == nil || !strings.Contains(err.Error(), "pippit-tool-cli login") || calls != 0 {
|
|
126
|
+
t.Fatalf("missing/expired: err=%v calls=%d", err, calls)
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
func TestMarketingPreviewAndHelpDoNotReadCredentials(t *testing.T) {
|
|
133
|
+
for _, args := range [][]string{{"marketing", "--help"}, {"marketing", "generate", "--request", "-"}, {"marketing", "query"}} {
|
|
134
|
+
cfg := config.Load()
|
|
135
|
+
cfg.AccessKey = ""
|
|
136
|
+
store := &marketingCredentialStore{}
|
|
137
|
+
runner := newRootRunner(cfg)
|
|
138
|
+
runner.Auth = auth.NewManager(cfg, auth.WithCredentialStore(store))
|
|
139
|
+
root := newRootCommand(io.Discard, io.Discard, runner)
|
|
140
|
+
root.SetIn(strings.NewReader(marketingRequest))
|
|
141
|
+
root.SetArgs(args)
|
|
142
|
+
err := root.Execute()
|
|
143
|
+
if args[1] != "query" && err != nil {
|
|
144
|
+
t.Fatal(err)
|
|
145
|
+
}
|
|
146
|
+
if args[1] == "query" && err == nil {
|
|
147
|
+
t.Fatal("missing IDs accepted")
|
|
148
|
+
}
|
|
149
|
+
if store.loads != 0 {
|
|
150
|
+
t.Fatal("offline operation accessed credentials")
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
func TestMarketingDoesNotReplaySubmission(t *testing.T) {
|
|
156
|
+
for _, status := range []int{302, 307, 504} {
|
|
157
|
+
t.Run(fmt.Sprint(status), func(t *testing.T) {
|
|
158
|
+
calls := 0
|
|
159
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
160
|
+
calls++
|
|
161
|
+
w.Header().Set("Location", "/unexpected")
|
|
162
|
+
w.WriteHeader(status)
|
|
163
|
+
}))
|
|
164
|
+
defer server.Close()
|
|
165
|
+
cfg := config.Load()
|
|
166
|
+
cfg.BaseURL = server.URL
|
|
167
|
+
cfg.AccessKey = "test-key"
|
|
168
|
+
root := newRootCommand(io.Discard, io.Discard, newRootRunner(cfg))
|
|
169
|
+
root.SetIn(strings.NewReader(marketingRequest))
|
|
170
|
+
root.SetArgs([]string{"marketing", "generate", "--request", "-", "--execute"})
|
|
171
|
+
if err := root.Execute(); err == nil {
|
|
172
|
+
t.Fatal("failed/redirected request succeeded")
|
|
173
|
+
}
|
|
174
|
+
if calls != 1 {
|
|
175
|
+
t.Fatalf("submission replayed %d times", calls)
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
}
|
package/cmd/root.go
CHANGED
|
@@ -73,6 +73,7 @@ func newRootCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Comm
|
|
|
73
73
|
root.AddCommand(newDownloadResultCommand(stdout, stderr, runner))
|
|
74
74
|
root.AddCommand(newGetCreditBalanceCommand(stdout, stderr, runner))
|
|
75
75
|
root.AddCommand(newModelCommand(stdout, stderr, runner))
|
|
76
|
+
root.AddCommand(newMarketingCommand(stdout, stderr, runner))
|
|
76
77
|
root.AddCommand(newGetThreadCommand(stdout, stderr, runner))
|
|
77
78
|
root.AddCommand(newSubmitRunCommand(stdout, stderr, runner))
|
|
78
79
|
root.AddCommand(newUploadFileCommand(stdout, stderr, runner))
|
package/cmd/update/update.go
CHANGED
|
@@ -101,7 +101,8 @@ func installSkills(root string, stderr io.Writer) error {
|
|
|
101
101
|
} else if !info.IsDir() {
|
|
102
102
|
return fmt.Errorf("%s 不是目录", filepath.Join(root, "skills"))
|
|
103
103
|
}
|
|
104
|
-
|
|
104
|
+
// Override inherited npm global mode without changing skills add's -g scope.
|
|
105
|
+
if err := runInherit(stderr, "npx", "--global=false", "-y", "skills", "add", root, "-g", "-y", "--skill", "*"); err != nil {
|
|
105
106
|
return err
|
|
106
107
|
}
|
|
107
108
|
return cleanupLegacyGlobalSkills(defaultGlobalSkillsDir())
|
|
@@ -26,13 +26,14 @@ func TestInstallSkillsInstallsAllBundledSkills(t *testing.T) {
|
|
|
26
26
|
binDir := t.TempDir()
|
|
27
27
|
capturePath := filepath.Join(t.TempDir(), "args.txt")
|
|
28
28
|
npxPath := filepath.Join(binDir, "npx")
|
|
29
|
-
script := "#!/bin/sh\nfor arg in \"$@\"; do printf '%s\\n' \"$arg\"; done > \"$CAPTURE_ARGS\"\n"
|
|
29
|
+
script := "#!/bin/sh\nif [ \"$npm_config_global\" = true ] && [ \"$1\" != --global=false ]; then exit 1; fi\nfor arg in \"$@\"; do printf '%s\\n' \"$arg\"; done > \"$CAPTURE_ARGS\"\n"
|
|
30
30
|
if err := os.WriteFile(npxPath, []byte(script), 0o755); err != nil {
|
|
31
31
|
t.Fatal(err)
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
35
35
|
t.Setenv("CAPTURE_ARGS", capturePath)
|
|
36
|
+
t.Setenv("npm_config_global", "true")
|
|
36
37
|
t.Setenv("HOME", t.TempDir())
|
|
37
38
|
|
|
38
39
|
var stderr bytes.Buffer
|
|
@@ -45,7 +46,7 @@ func TestInstallSkillsInstallsAllBundledSkills(t *testing.T) {
|
|
|
45
46
|
t.Fatal(err)
|
|
46
47
|
}
|
|
47
48
|
got := strings.Split(strings.TrimSpace(string(gotBytes)), "\n")
|
|
48
|
-
want := []string{"-y", "skills", "add", root, "-g", "-y", "--skill", "*"}
|
|
49
|
+
want := []string{"--global=false", "-y", "skills", "add", root, "-g", "-y", "--skill", "*"}
|
|
49
50
|
if strings.Join(got, "\n") != strings.Join(want, "\n") {
|
|
50
51
|
t.Fatalf("npx args mismatch\ngot: %#v\nwant: %#v", got, want)
|
|
51
52
|
}
|
package/dist/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
2f3c2a486a66e408a6d3841f19ecb909fbaa657636a469ee782a424e59596a68 pippit-tool-cli-1.0.32-darwin-amd64.tar.gz
|
|
2
|
+
f4ccd97a007167a9677ea85be52339a041f5448c4e4afd2ec996d32006ccec83 pippit-tool-cli-1.0.32-darwin-arm64.tar.gz
|
|
3
|
+
91ae74eb2067844be741aa8a5f5f932152214c168d43efae801244d46e3f5cab pippit-tool-cli-1.0.32-linux-amd64.tar.gz
|
|
4
|
+
1928d81ea0f11fa20ae65a689654a9d2dddc4cacb4e6a5ae41a5cdd35e40eac5 pippit-tool-cli-1.0.32-linux-arm64.tar.gz
|
|
5
|
+
79283336e84c3196454aecf96c367383795636f4e153234a11996124a78dd7a5 pippit-tool-cli-1.0.32-windows-amd64.zip
|
|
6
|
+
aa73d963a3dd7d0eb5dba433fdf14d97510dfbe90515aab5318e55c0147d14d9 pippit-tool-cli-1.0.32-windows-arm64.zip
|
|
@@ -46,7 +46,17 @@ func NewHTTPClient(baseURL string, timeout time.Duration, authorizer RequestAuth
|
|
|
46
46
|
return newHTTPClient(baseURL, timeout, authorizer)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
// NewNonRedirectingHTTPClient keeps one-shot submissions from being replayed.
|
|
50
|
+
// Authentication and request handling still use the shared CLI client.
|
|
51
|
+
func NewNonRedirectingHTTPClient(baseURL string, timeout time.Duration, authorizer RequestAuthorizer) Client {
|
|
52
|
+
client := newHTTPClient(baseURL, timeout, authorizer)
|
|
53
|
+
client.httpClient.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
|
|
54
|
+
return fmt.Errorf("拒绝营销 API 重定向;请求未重试")
|
|
55
|
+
}
|
|
56
|
+
return client
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func newHTTPClient(baseURL string, timeout time.Duration, authorizer RequestAuthorizer) *httpClient {
|
|
50
60
|
client := &httpClient{
|
|
51
61
|
baseURL: strings.TrimRight(baseURL, "/"),
|
|
52
62
|
headers: make(http.Header),
|
package/package.json
CHANGED
package/scripts/skills.js
CHANGED
|
@@ -28,7 +28,8 @@ function installSkillsFromRoot(root, opts = {}) {
|
|
|
28
28
|
if (!fs.existsSync(skillsDir)) {
|
|
29
29
|
throw new Error(`skills directory not found: ${skillsDir}`);
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
// Override npm install -g's inherited mode; the later -g belongs to skills.
|
|
32
|
+
run("npx", ["--global=false", "-y", "skills", "add", source, "-g", "-y", "--skill", "*"], {
|
|
32
33
|
timeout: opts.timeout || 120000,
|
|
33
34
|
});
|
|
34
35
|
cleanupLegacyGlobalSkills(opts.globalSkillsDir);
|
|
@@ -3,7 +3,7 @@ name: xyq-marketing-skill
|
|
|
3
3
|
description: 使用小云雀公开营销 API,根据商品图文生成剧情广告、品牌大片或达人带货营销视频;上传素材、提交营销成片、查询进度、下载交付结果及查询积分。用户要求小云雀营销一键成片或接入营销 API 时使用。
|
|
4
4
|
user-invocable: true
|
|
5
5
|
metadata:
|
|
6
|
-
{"openclaw": {"emoji": "🛍️", "requires": {"bins": ["node"
|
|
6
|
+
{"openclaw": {"emoji": "🛍️", "requires": {"bins": ["node", "pippit-tool-cli"]}}}
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# 小云雀营销成片
|
|
@@ -12,9 +12,11 @@ metadata:
|
|
|
12
12
|
|
|
13
13
|
## Setup
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
需要 Node.js 16+ 和支持 `marketing` 命令的 `@pippit-dev/cli`。脚本位置为 `"{baseDir}/scripts/marketing.js"`;下面示例中的相对路径从本 Skill 目录执行。先检查 `pippit-tool-cli marketing --help`;命令不存在时安装或更新 CLI:`npm install -g @pippit-dev/cli@latest`,再检查一次;仍不支持就报告版本阻塞,不改为向用户索要密钥。
|
|
16
16
|
|
|
17
|
-
API
|
|
17
|
+
营销 API 调用复用其它 CLI 命令的登录态,脚本不读取或导出凭据。执行真实请求前运行 `pippit-tool-cli status`,读取 JSON 的 `logged_in`,不能只看退出码。未登录或凭据过期时运行 `pippit-tool-cli login`,让用户在浏览器完成授权;等待 CLI 成功返回后再次检查 status,再继续原任务。已有有效登录态直接复用,不重复登录;离线预览和帮助不要求登录。
|
|
18
|
+
|
|
19
|
+
不要让用户提供或复制 `access_token`、Access Key,也不要把凭据写入聊天、请求文件、命令参数或日志。CLI 保留原有 `XYQ_ACCESS_KEY` 显式环境覆盖规则,优先于网页登录;这仅用于已配置的自动化环境,不作为普通用户的必填项。覆盖无效时不能静默切换账号。网页登录凭据被拒绝时按 CLI 的 `login --force` 流程处理,不自动重提可能已创建的生成任务。
|
|
18
20
|
|
|
19
21
|
先按 [接口契约](references/api.md) 准备请求;需要完整执行示例时读 [商品图到营销视频](examples/product-video.md)。
|
|
20
22
|
|
|
@@ -24,7 +26,7 @@ API 和 CLI 可以使用同一个 Access Key,但脚本只读取当前进程的
|
|
|
24
26
|
2. 有本地商品素材时逐个上传,保留返回的 `data.pippit_asset_id`。远程素材先取得用户授权使用的本地文件;不能把 URL 或路径放进 `asset_ids`。用户已提供有效资产 ID 时直接复用。无素材的纯文字请求无需上传。
|
|
25
27
|
3. 写入 UTF-8 JSON 请求文件。`message` 保留用户指令;`general_agent_settings.video_model` 必填,不能传 `{}`。用户指定模型时原样使用;未指定时询问,或在用户已明确授权“你决定”等选择范围内选定并说明。其它选项按用户给定或已授权的偏好设置,不静默换模型。`thread_id` 仅在继续已有营销会话时传入真实 ID。
|
|
26
28
|
4. 先预览校验,已获生成授权后使用 `--execute` 提交。立即保存响应并展示真实 `data.web_thread_link`,保留 `data.run.thread_id`、`data.run.run_id`。缺少网页链接时只报告实际返回信息,不自行拼接链接。
|
|
27
|
-
5. 用当前 thread/run 查询到结束并下载媒体。遇到确认或问卷时沿用已有授权;缺少必要选择再问用户。确认后继续同一 thread,并取得最新 run_id 再查询:旧 Run
|
|
29
|
+
5. 用当前 thread/run 查询到结束并下载媒体。遇到确认或问卷时沿用已有授权;缺少必要选择再问用户。确认后继续同一 thread,并取得最新 run_id 再查询:旧 Run 可永久保持等待交互状态。可通过宿主浏览器查看已返回的网页链接;也可用 `pippit-tool-cli` 的 `get-thread --thread-id THREAD_ID` 读取各 Run 的真实 ID。不要用旧 Run 重复确认或重新生成。提交成功、网页链接或进度链接都不等于交付完成。
|
|
28
30
|
|
|
29
31
|
```bash
|
|
30
32
|
node scripts/marketing.js upload --file /path/to/product.png
|
|
@@ -34,7 +36,7 @@ node scripts/marketing.js query --thread-id THREAD_ID --run-id RUN_ID --wait --m
|
|
|
34
36
|
node scripts/marketing.js balance
|
|
35
37
|
```
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
生成默认是离线预览;预览不需要登录。上传、查询、余额是实际 API 请求。`--timeout` 设置单请求总时限(秒,默认 60)。`--wait` 由脚本每 10 秒查询,默认最多 900 秒;不再叠加其它轮询器。没有 `--wait` 时只查询一次。脚本输出逐行 JSON,查询下载前先输出服务端响应,再逐个输出已下载文件,最后输出含 `downloaded_files` 的响应。
|
|
38
40
|
|
|
39
41
|
## Completion and Recovery
|
|
40
42
|
|
|
@@ -48,4 +50,4 @@ node scripts/marketing.js balance
|
|
|
48
50
|
|
|
49
51
|
## Scope
|
|
50
52
|
|
|
51
|
-
本 Skill
|
|
53
|
+
本 Skill 仅使用正式公开营销接口;不宣称支持团队空间切换。鉴权范围沿用当前 CLI 登录身份和服务端授权,公开请求没有 `TeamID` 字段,不自行加入团队字段或跨账号复用资产/任务 ID。沉浸式短片、火山引擎服务和现有 CLI 的来源统计参数不在此脚本范围;不要把 `--source` 等未公开字段传给营销接口。
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
用户示例:“用这张保温杯商品图做一个 15 秒竖屏达人带货视频,不要字幕,用 Seedance 2.0 VIP,1080p。”
|
|
4
4
|
|
|
5
|
-
1.
|
|
5
|
+
1. 先检查 `pippit-tool-cli marketing --help` 与 `pippit-tool-cli status`;未登录时执行 `pippit-tool-cli login` 并等待浏览器授权成功,无需用户提供 token。随后上传用户给出的真实文件:
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
node scripts/marketing.js upload --file /path/to/cup.png
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
## Endpoints
|
|
6
6
|
|
|
7
|
-
Base URL:`https://xyq.jianying.com`。全部使用 POST
|
|
7
|
+
Base URL:`https://xyq.jianying.com`。全部使用 POST,`Accept: application/json`。CLI 的统一认证层从现有登录态注入 Authorization 请求头;营销脚本不接触凭据,不要求用户提供 token。
|
|
8
8
|
|
|
9
9
|
| 操作 | 路径 | 请求体 |
|
|
10
10
|
| --- | --- | --- |
|
|
11
|
-
| 上传单文件 | `/api/biz/v1/skill/upload_file` | multipart/form-data,字段 `file`,boundary
|
|
11
|
+
| 上传单文件 | `/api/biz/v1/skill/upload_file` | multipart/form-data,字段 `file`,boundary 由 CLI 生成 |
|
|
12
12
|
| 营销成片 | `/api/biz/v1/agent/submit_marketing_run` | JSON,字段见下表 |
|
|
13
13
|
| 查询结果 | `/api/biz/v1/agent/query_generate_video_result` | JSON:真实 `thread_id`、`run_id` |
|
|
14
14
|
| 查询积分 | `/api/biz/v1/skill/get_credit_balance` | JSON:`{}` |
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
2
|
+
// Marketing orchestration; authenticated API calls stay inside pippit-tool-cli.
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const https = require('https');
|
|
6
6
|
const crypto = require('crypto');
|
|
7
|
-
const {
|
|
7
|
+
const { spawn } = require('child_process');
|
|
8
8
|
const { pipeline } = require('stream/promises');
|
|
9
9
|
|
|
10
10
|
const BASE = 'https://xyq.jianying.com';
|
|
@@ -63,7 +63,64 @@ function checkResponse(action, result, body) {
|
|
|
63
63
|
return result;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
function
|
|
66
|
+
function resolveCLI({ platform = process.platform, searchPath = process.env.PATH || '', packageRoot = path.resolve(__dirname, '../../..') } = {}) {
|
|
67
|
+
const binaryName = platform === 'win32' ? 'pippit-tool-cli.exe' : 'pippit-tool-cli';
|
|
68
|
+
function fromPackage(root) {
|
|
69
|
+
const manifest = path.join(root, 'package.json');
|
|
70
|
+
if (!fs.existsSync(manifest) || JSON.parse(fs.readFileSync(manifest, 'utf8')).name !== '@pippit-dev/cli') return null;
|
|
71
|
+
const command = path.join(root, 'bin', binaryName);
|
|
72
|
+
requireValue(fs.existsSync(command), 'CLI 原生程序缺失,请重新安装 @pippit-dev/cli;无需提供 access_token');
|
|
73
|
+
return { command, args: [] };
|
|
74
|
+
}
|
|
75
|
+
// Run the native binary directly so deadlines also stop the API process.
|
|
76
|
+
const bundled = fromPackage(packageRoot);
|
|
77
|
+
if (bundled) return bundled;
|
|
78
|
+
for (const dir of searchPath.split(path.delimiter).filter(Boolean)) {
|
|
79
|
+
const binary = path.join(dir, binaryName);
|
|
80
|
+
if (fs.existsSync(binary)) {
|
|
81
|
+
const resolved = fs.realpathSync(binary);
|
|
82
|
+
const npmPackage = fromPackage(path.resolve(path.dirname(resolved), '..'));
|
|
83
|
+
return npmPackage || { command: binary, args: [] };
|
|
84
|
+
}
|
|
85
|
+
if (platform === 'win32') {
|
|
86
|
+
const npmPackage = fromPackage(path.join(dir, 'node_modules/@pippit-dev/cli'));
|
|
87
|
+
if (npmPackage) return npmPackage;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
throw new Error('请先安装或更新 @pippit-dev/cli,并运行 pippit-tool-cli login;无需提供 access_token');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function invokeCLI(args, input, timeout, invocation = resolveCLI()) {
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
const child = spawn(invocation.command, [...invocation.args, ...args], {
|
|
96
|
+
shell: false, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'],
|
|
97
|
+
});
|
|
98
|
+
let stdout = '', stderr = '', failure;
|
|
99
|
+
const stop = message => { failure = new Error(message); child.kill(); };
|
|
100
|
+
const timer = setTimeout(() => stop('CLI 请求超时;提交结果可能不明确,请勿自动重提'), timeout);
|
|
101
|
+
child.stdout.setEncoding('utf8'); child.stderr.setEncoding('utf8');
|
|
102
|
+
child.stdout.on('data', chunk => {
|
|
103
|
+
stdout += chunk;
|
|
104
|
+
if (Buffer.byteLength(stdout) > 8 * 1024 * 1024) stop('CLI 响应超出 8 MiB 限制');
|
|
105
|
+
});
|
|
106
|
+
child.stderr.on('data', chunk => {
|
|
107
|
+
stderr += chunk;
|
|
108
|
+
if (Buffer.byteLength(stderr) > 1024 * 1024) stop('CLI 错误输出过大');
|
|
109
|
+
});
|
|
110
|
+
child.once('error', () => { clearTimeout(timer); reject(new Error('无法启动 pippit-tool-cli,请检查安装;无需提供 access_token')); });
|
|
111
|
+
child.stdin.on('error', error => { if (error.code !== 'EPIPE') failure = new Error('无法写入 CLI 请求'); });
|
|
112
|
+
child.once('close', code => {
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
if (failure) return reject(failure);
|
|
115
|
+
if (code !== 0) return reject(new Error(`CLI 调用失败:${stderr.trim() || `退出码 ${code}`};请求未重试。若不支持 marketing 命令,请更新 CLI。`));
|
|
116
|
+
try { resolve(JSON.parse(stdout)); }
|
|
117
|
+
catch (_) { reject(new Error('CLI 未返回有效 JSON;提交结果可能不明确,请勿自动重提')); }
|
|
118
|
+
});
|
|
119
|
+
child.stdin.end(input);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function createClient({ request = https.request, timeout = 60000, invoke = invokeCLI } = {}) {
|
|
67
124
|
function open(url, method, headers, body) {
|
|
68
125
|
return new Promise((resolve, reject) => {
|
|
69
126
|
const target = new URL(url);
|
|
@@ -84,42 +141,23 @@ function createClient({ key = process.env.XYQ_ACCESS_KEY, request = https.reques
|
|
|
84
141
|
});
|
|
85
142
|
}
|
|
86
143
|
|
|
87
|
-
async function api(action, body
|
|
88
|
-
requireValue(
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
const chunks = [];
|
|
97
|
-
let size = 0;
|
|
98
|
-
for await (const chunk of res) {
|
|
99
|
-
size += chunk.length;
|
|
100
|
-
requireValue(size <= 8 * 1024 * 1024, 'API 响应超出 8 MiB 限制');
|
|
101
|
-
chunks.push(chunk);
|
|
144
|
+
async function api(action, body) {
|
|
145
|
+
requireValue(['generate', 'query', 'balance'].includes(action), '无效营销 API 操作');
|
|
146
|
+
const args = ['marketing', action, '--timeout', `${timeout}ms`];
|
|
147
|
+
let input;
|
|
148
|
+
if (action === 'generate') {
|
|
149
|
+
validate(body);
|
|
150
|
+
args.push('--request', '-', '--execute');
|
|
151
|
+
input = JSON.stringify(body);
|
|
102
152
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
catch (_) { throw new Error('API 未返回有效 JSON;提交结果可能不明确,请勿自动重提'); }
|
|
106
|
-
return checkResponse(action, result, body);
|
|
153
|
+
if (action === 'query') args.push('--thread-id', body.thread_id, '--run-id', body.run_id);
|
|
154
|
+
return checkResponse(action, await invoke(args, input, timeout), body);
|
|
107
155
|
}
|
|
108
156
|
|
|
109
157
|
async function upload(file) {
|
|
110
158
|
const stat = await fs.promises.stat(file);
|
|
111
159
|
requireValue(stat.isFile() && stat.size > 0 && stat.size < 500000000, '上传需要非空文件且小于 500 MB');
|
|
112
|
-
|
|
113
|
-
const name = path.basename(file).replace(/["\r\n\\]/g, '_');
|
|
114
|
-
const mime = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.mp4': 'video/mp4', '.mp3': 'audio/mpeg', '.wav': 'audio/wav' }[path.extname(file).toLowerCase()] || 'application/octet-stream';
|
|
115
|
-
const head = Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${name}"\r\nContent-Type: ${mime}\r\n\r\n`);
|
|
116
|
-
const tail = Buffer.from(`\r\n--${boundary}--\r\n`);
|
|
117
|
-
const body = Readable.from((async function* () {
|
|
118
|
-
yield head;
|
|
119
|
-
for await (const chunk of fs.createReadStream(file)) yield chunk;
|
|
120
|
-
yield tail;
|
|
121
|
-
})());
|
|
122
|
-
return api('upload', body, { 'Content-Type': `multipart/form-data; boundary=${boundary}`, 'Content-Length': head.length + stat.size + tail.length });
|
|
160
|
+
return checkResponse('upload', await invoke(['marketing', 'upload', '--file', path.resolve(file), '--timeout', `${timeout}ms`], undefined, timeout));
|
|
123
161
|
}
|
|
124
162
|
|
|
125
163
|
async function download(url, destination) {
|
|
@@ -159,7 +197,7 @@ const HELP = `小云雀营销 Skill(Node.js >= 16)
|
|
|
159
197
|
node marketing.js upload --file product.png
|
|
160
198
|
node marketing.js query --thread-id ID --run-id ID [--wait] [--max-wait 900] [--output-dir DIR]
|
|
161
199
|
node marketing.js balance
|
|
162
|
-
所有 API
|
|
200
|
+
所有 API 调用复用 pippit-tool-cli 登录态;未登录先运行 pippit-tool-cli login,无需提供 access_token。generate 默认仅预览。
|
|
163
201
|
--timeout 秒数:单请求总时限,默认 60;--max-wait:轮询总时限,默认 900。
|
|
164
202
|
query 输出 API 原始响应;有 --output-dir 时成功结果附带 downloaded_files。
|
|
165
203
|
退出码:0 成功/单次查询进行中;1 输入或接口错误;2 生成失败/取消/无视频;3 等待超时;4 等待用户交互;5 未知或未指定状态。
|
|
@@ -189,7 +227,7 @@ function parseArgs(argv) {
|
|
|
189
227
|
return { action, options };
|
|
190
228
|
}
|
|
191
229
|
|
|
192
|
-
async function main(argv, {
|
|
230
|
+
async function main(argv, { out = console.log, clientFactory = createClient, now = Date.now, pause = ms => new Promise(resolve => setTimeout(resolve, ms)) } = {}) {
|
|
193
231
|
if (!argv.length || argv.includes('--help') || argv.includes('-h')) { out(HELP); return 0; }
|
|
194
232
|
const { action, options } = parseArgs(argv);
|
|
195
233
|
let body = {};
|
|
@@ -203,13 +241,13 @@ async function main(argv, { env = process.env, out = console.log, clientFactory
|
|
|
203
241
|
body = { thread_id: options['thread-id'], run_id: options['run-id'] };
|
|
204
242
|
}
|
|
205
243
|
if (action === 'upload') requireValue(nonempty(options.file), 'upload 需要 --file');
|
|
206
|
-
const client = clientFactory({
|
|
244
|
+
const client = clientFactory({ timeout: options.timeout * 1000 });
|
|
207
245
|
let result;
|
|
208
246
|
const deadline = now() + options['max-wait'] * 1000;
|
|
209
247
|
do {
|
|
210
248
|
// A poll request cannot overrun the remaining wait budget.
|
|
211
249
|
const pollClient = action === 'query' && options.wait
|
|
212
|
-
? clientFactory({
|
|
250
|
+
? clientFactory({ timeout: Math.min(options.timeout * 1000, Math.max(1, deadline - now())) }) : client;
|
|
213
251
|
result = action === 'upload' ? await client.upload(options.file) : await pollClient.api(action, body);
|
|
214
252
|
if (action !== 'query' || !options.wait || !WAIT_STATES.includes(String(result.data.run_state))) break;
|
|
215
253
|
if (now() >= deadline) {
|
|
@@ -265,4 +303,4 @@ if (require.main === module) {
|
|
|
265
303
|
process.exitCode = 1;
|
|
266
304
|
});
|
|
267
305
|
}
|
|
268
|
-
module.exports = { BASE, PATHS, validate, checkResponse, createClient, parseArgs, main };
|
|
306
|
+
module.exports = { BASE, PATHS, validate, checkResponse, resolveCLI, invokeCLI, createClient, parseArgs, main };
|