@pippit-dev/cli 1.0.41 → 1.0.43
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 +42 -11
- package/checksums.txt +6 -6
- package/cmd/auth/auth.go +19 -5
- package/cmd/auth/auth_test.go +23 -0
- package/cmd/canvas/canvas.go +7 -7
- package/cmd/download_result.go +3 -2
- package/cmd/marketing.go +19 -3
- package/cmd/marketing_test.go +11 -5
- package/cmd/model.go +5 -4
- package/cmd/root.go +17 -5
- package/cmd/root_update_check_test.go +139 -0
- package/cmd/short_drama_test.go +2 -0
- package/cmd/source_test.go +27 -4
- package/cmd/space.go +199 -0
- package/cmd/space_test.go +469 -0
- package/cmd/upload_file_test.go +3 -1
- package/dist/checksums.txt +6 -6
- package/internal/adaptercli/commands_test.go +9 -5
- package/internal/auth/loopback.go +16 -11
- package/internal/auth/loopback_origins_test.go +152 -0
- package/internal/auth/space.go +26 -0
- package/internal/auth/space_test.go +42 -0
- package/internal/canvas/get.go +9 -2
- package/internal/commandnames/names.go +9 -4
- package/internal/common/client.go +23 -0
- package/internal/common/host_source.go +5 -0
- package/internal/common/host_source_test.go +9 -1
- package/internal/common/runner.go +5 -3
- package/internal/common/space.go +185 -0
- package/internal/common/space_test.go +132 -0
- package/internal/common/submit_run.go +2 -0
- package/internal/common/upload_file.go +2 -0
- package/internal/config/config.go +15 -12
- package/internal/generate_image/generate_image.go +3 -9
- package/internal/short_drama/submit_run.go +3 -9
- package/internal/space/session.go +153 -0
- package/internal/space/store.go +211 -0
- package/internal/space/store_test.go +246 -0
- package/internal/version/version.go +13 -0
- package/internal/version/version_test.go +38 -0
- package/internal/versioncheck/check.go +147 -0
- package/internal/versioncheck/check_test.go +204 -0
- package/internal/versioncheck/semver.go +84 -0
- package/package.json +1 -1
- package/scripts/canvas-command.js +47 -5
- package/scripts/install-wizard.js +1 -1
- package/scripts/run.js +15 -4
- package/scripts/telemetry.js +3 -1
- package/scripts/version-check.js +8 -1
- package/skills/short-drama/SKILL.md +12 -1
- package/skills/xyq-marketing-skill/SKILL.md +22 -3
- package/skills/xyq-marketing-skill/references/api.md +7 -1
- package/skills/xyq-marketing-skill/scripts/marketing.js +51 -19
- package/skills/xyq-nest-skill/SKILL.md +15 -4
- package/skills/xyq-nest-skill/commands/canvas-assets.md +1 -1
- package/skills/xyq-nest-skill/commands/canvas.md +5 -3
- package/skills/xyq-nest-skill/commands/get-credit-balance.md +2 -2
- package/skills/xyq-nest-skill/commands/model.md +1 -1
- package/skills/xyq-nest-skill/scripts/ensure-cli.js +8 -1
- package/skills/xyq-nest-skill/scripts/install.md +1 -1
- package/skills/xyq-nest-skill/workflows/canvas-edit.md +2 -2
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"errors"
|
|
8
|
+
"io"
|
|
9
|
+
"net/http"
|
|
10
|
+
"net/http/httptest"
|
|
11
|
+
"os"
|
|
12
|
+
"path/filepath"
|
|
13
|
+
"strings"
|
|
14
|
+
"testing"
|
|
15
|
+
"time"
|
|
16
|
+
|
|
17
|
+
"github.com/Pippit-dev/pippit-cli/internal/adapter"
|
|
18
|
+
"github.com/Pippit-dev/pippit-cli/internal/auth"
|
|
19
|
+
canvascore "github.com/Pippit-dev/pippit-cli/internal/canvas"
|
|
20
|
+
"github.com/Pippit-dev/pippit-cli/internal/common"
|
|
21
|
+
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
22
|
+
"github.com/Pippit-dev/pippit-cli/internal/space"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
func TestSpaceCommandsAndBusinessTaskScope(t *testing.T) {
|
|
26
|
+
t.Setenv("HOME", t.TempDir())
|
|
27
|
+
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
|
28
|
+
t.Setenv("LocalAppData", t.TempDir())
|
|
29
|
+
var requests []string
|
|
30
|
+
var requestRef space.References
|
|
31
|
+
listFailure := false
|
|
32
|
+
listTeamID := ""
|
|
33
|
+
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
34
|
+
requests = append(requests, r.URL.Path+":"+r.Header.Get("X-Team-Id"))
|
|
35
|
+
w.Header().Set("Content-Type", "application/json")
|
|
36
|
+
if r.URL.Path == config.GetLiteTeamListPath {
|
|
37
|
+
if r.Header.Get("X-Team-Id") != listTeamID {
|
|
38
|
+
t.Errorf("team list header = %q, want %q", r.Header.Get("X-Team-Id"), listTeamID)
|
|
39
|
+
}
|
|
40
|
+
var body struct {
|
|
41
|
+
Cursor string `json:"cursor"`
|
|
42
|
+
Count int `json:"count"`
|
|
43
|
+
}
|
|
44
|
+
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
45
|
+
t.Error(err)
|
|
46
|
+
}
|
|
47
|
+
if body.Count != 100 {
|
|
48
|
+
t.Error("unexpected page size")
|
|
49
|
+
}
|
|
50
|
+
if listFailure {
|
|
51
|
+
_, _ = w.Write([]byte(`{"ret":"101","errmsg":"denied"}`))
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
if body.Cursor == "0" {
|
|
55
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"team_info_list":[{"team_id":"12","team_name":"A","current_role":1}],"has_more":true,"next_cursor":"next"}}`))
|
|
56
|
+
} else {
|
|
57
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"team_info_list":[{"team_id":"34","team_name":"B"}],"has_more":false}}`))
|
|
58
|
+
}
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
if r.URL.Path == config.UploadFilePath {
|
|
62
|
+
_, _ = io.Copy(io.Discard, r.Body)
|
|
63
|
+
} else {
|
|
64
|
+
requestRef = space.References{}
|
|
65
|
+
if err := json.NewDecoder(r.Body).Decode(&requestRef); err != nil {
|
|
66
|
+
t.Error(err)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
switch r.URL.Path {
|
|
70
|
+
case canvascore.QueryPath:
|
|
71
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"Assets":[{"PippitAssetID":"canvas"}]}}`))
|
|
72
|
+
case config.SubmitRunPath:
|
|
73
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"run":{"thread_id":"thread-a","run_id":"run-a"}}}`))
|
|
74
|
+
case config.GetThreadPath:
|
|
75
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"readable_text":"task result"}}`))
|
|
76
|
+
case config.ListThreadFilePath:
|
|
77
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"files":[],"total":0}}`))
|
|
78
|
+
case config.UploadFilePath:
|
|
79
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"pippit_asset_id":"asset-a"}}`))
|
|
80
|
+
case marketingPaths["generate"]:
|
|
81
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"run":{"thread_id":"marketing-thread","run_id":"marketing-run"}}}`))
|
|
82
|
+
case marketingPaths["query"]:
|
|
83
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"thread_id":"marketing-thread","run_id":"marketing-run","run_state":2}}`))
|
|
84
|
+
case config.GetCreditBalancePath:
|
|
85
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"total_remain_amount":"100"}}`))
|
|
86
|
+
case config.GetAvailableModelListPath:
|
|
87
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"scene":"web_turbo_video_generator","config_key":"key","config":{"models":[{"key":"MiniMax-H3","name":"MiniMax","kind":"video","supported_ratio_list":[0,2,3],"default_ratio":3,"audio_total_limit":0}]}}}`))
|
|
88
|
+
default:
|
|
89
|
+
t.Errorf("unexpected request %s", r.URL.Path)
|
|
90
|
+
}
|
|
91
|
+
}))
|
|
92
|
+
defer api.Close()
|
|
93
|
+
cfg := config.Load()
|
|
94
|
+
cfg.BaseURL, cfg.AccessKey = api.URL, ""
|
|
95
|
+
runner := newRootRunner(cfg)
|
|
96
|
+
credentials := &marketingCredentialStore{credential: &auth.Credential{UID: "space-user", AccessKey: "space-test-key", ExpiredAt: time.Now().Add(time.Hour).Unix()}}
|
|
97
|
+
runner.Auth = auth.NewManager(cfg, auth.WithCredentialStore(credentials))
|
|
98
|
+
runner.SpaceStore = &space.Store{Dir: t.TempDir()}
|
|
99
|
+
store, err := common.SpaceStoreForRunner(context.Background(), runner)
|
|
100
|
+
if err != nil {
|
|
101
|
+
t.Fatal(err)
|
|
102
|
+
}
|
|
103
|
+
execute := func(args ...string) (string, error) {
|
|
104
|
+
var out, warnings bytes.Buffer
|
|
105
|
+
root := newRootCommand(&out, &warnings, runner)
|
|
106
|
+
root.SetIn(strings.NewReader(""))
|
|
107
|
+
root.SetArgs(args)
|
|
108
|
+
err := root.Execute()
|
|
109
|
+
return out.String(), err
|
|
110
|
+
}
|
|
111
|
+
must := func(args ...string) string {
|
|
112
|
+
t.Helper()
|
|
113
|
+
out, err := execute(args...)
|
|
114
|
+
if err != nil {
|
|
115
|
+
t.Fatalf("%v: %v", args, err)
|
|
116
|
+
}
|
|
117
|
+
return out
|
|
118
|
+
}
|
|
119
|
+
checkList := func(teamID string) {
|
|
120
|
+
t.Helper()
|
|
121
|
+
var result struct {
|
|
122
|
+
Spaces []listedSpace `json:"spaces"`
|
|
123
|
+
Current *space.Selection `json:"current"`
|
|
124
|
+
}
|
|
125
|
+
out := must("space", "list")
|
|
126
|
+
if err := json.Unmarshal([]byte(out), &result); err != nil {
|
|
127
|
+
t.Fatal(err)
|
|
128
|
+
}
|
|
129
|
+
if result.Current == nil || result.Current.TeamID != teamID || len(result.Spaces) != 3 {
|
|
130
|
+
t.Fatalf("unexpected space list: %s", out)
|
|
131
|
+
}
|
|
132
|
+
for _, item := range result.Spaces {
|
|
133
|
+
if item.Current != (item.TeamID == teamID) {
|
|
134
|
+
t.Fatalf("wrong current marker: %+v", item)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
checkList("0")
|
|
139
|
+
must("space", "use", "12")
|
|
140
|
+
listTeamID = "12"
|
|
141
|
+
checkList("12")
|
|
142
|
+
file := filepath.Join(t.TempDir(), "image.png")
|
|
143
|
+
if err := os.WriteFile(file, []byte("media"), 0600); err != nil {
|
|
144
|
+
t.Fatal(err)
|
|
145
|
+
}
|
|
146
|
+
if out := must("upload-file", "--path", file); !strings.Contains(out, `"team_id":"12"`) {
|
|
147
|
+
t.Fatal(out)
|
|
148
|
+
}
|
|
149
|
+
if out := must("short-drama", "+submit-run", "--message", "create", "--asset-ids", "asset-a"); !strings.Contains(out, `"team_id":"12"`) {
|
|
150
|
+
t.Fatal(out)
|
|
151
|
+
}
|
|
152
|
+
must("space", "use", "34")
|
|
153
|
+
listTeamID = "34"
|
|
154
|
+
checkList("34")
|
|
155
|
+
must("model", "list", "--refresh")
|
|
156
|
+
if requests[len(requests)-1] != config.GetAvailableModelListPath+":34" {
|
|
157
|
+
t.Fatal("model query did not forward the selected team", requests)
|
|
158
|
+
}
|
|
159
|
+
must("model", "list", "--refresh", "--team-id", "0")
|
|
160
|
+
if requests[len(requests)-1] != config.GetAvailableModelListPath+":" {
|
|
161
|
+
t.Fatal("personal model query carried a team header", requests)
|
|
162
|
+
}
|
|
163
|
+
must("get-thread", "--thread-id", "thread-a", "--run-id", "run-a")
|
|
164
|
+
if requests[len(requests)-1] != config.GetThreadPath+":12" {
|
|
165
|
+
t.Fatal("old task used new default", requests)
|
|
166
|
+
}
|
|
167
|
+
for _, tc := range []struct {
|
|
168
|
+
args []string
|
|
169
|
+
path string
|
|
170
|
+
}{
|
|
171
|
+
{[]string{"get-thread", "--thread-id", " thread-a ", "--run-id", " run-a "}, config.GetThreadPath},
|
|
172
|
+
{[]string{"list-thread-file", "--thread-id", " thread-a "}, config.ListThreadFilePath},
|
|
173
|
+
{[]string{"short-drama", "+submit-run", "--thread-id", " thread-a ", "--message", "continue"}, config.SubmitRunPath},
|
|
174
|
+
} {
|
|
175
|
+
must(tc.args...)
|
|
176
|
+
if requests[len(requests)-1] != tc.path+":12" || requestRef.ThreadID != "thread-a" {
|
|
177
|
+
t.Fatalf("normalized task used wrong space: %v, %+v", requests, requestRef)
|
|
178
|
+
}
|
|
179
|
+
if tc.path == config.GetThreadPath && requestRef.RunID != "run-a" {
|
|
180
|
+
t.Fatalf("run ID was not normalized: %+v", requestRef)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
must("get-credit-balance")
|
|
184
|
+
if requests[len(requests)-1] != config.GetCreditBalancePath+":34" {
|
|
185
|
+
t.Fatal(requests)
|
|
186
|
+
}
|
|
187
|
+
must("get-credit-balance", "--team-id", "0")
|
|
188
|
+
if requests[len(requests)-1] != config.GetCreditBalancePath+":" {
|
|
189
|
+
t.Fatal(requests)
|
|
190
|
+
}
|
|
191
|
+
// Canvas uses the same default, override and cross-process binding as other commands.
|
|
192
|
+
for _, tc := range []struct {
|
|
193
|
+
flags []string
|
|
194
|
+
teamID string
|
|
195
|
+
}{
|
|
196
|
+
{nil, "34"},
|
|
197
|
+
{[]string{"--team-id", "12"}, "12"},
|
|
198
|
+
{[]string{"--team-id", "0"}, "0"},
|
|
199
|
+
} {
|
|
200
|
+
var result canvascore.GetResult
|
|
201
|
+
out := must(append([]string{"canvas", "get", "--asset-id", "canvas"}, tc.flags...)...)
|
|
202
|
+
if err := json.Unmarshal([]byte(out), &result); err != nil {
|
|
203
|
+
t.Fatal(err)
|
|
204
|
+
}
|
|
205
|
+
header := tc.teamID
|
|
206
|
+
if header == "0" {
|
|
207
|
+
header = ""
|
|
208
|
+
}
|
|
209
|
+
if result.TeamID != tc.teamID || requests[len(requests)-1] != canvascore.QueryPath+":"+header {
|
|
210
|
+
t.Fatalf("canvas space = %s, requests = %v", out, requests)
|
|
211
|
+
}
|
|
212
|
+
binding, err := space.DecodeBinding(result.SpaceContext)
|
|
213
|
+
if err != nil || binding.TeamID != tc.teamID || binding.Account != store.Account || binding.Origin != store.Origin {
|
|
214
|
+
t.Fatalf("canvas binding = %+v, %v", binding, err)
|
|
215
|
+
}
|
|
216
|
+
must("canvas", "get", "--asset-id", "canvas", "--space-context", result.SpaceContext)
|
|
217
|
+
if requests[len(requests)-1] != canvascore.QueryPath+":"+header {
|
|
218
|
+
t.Fatal("canvas binding did not preserve the original space", requests)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
before := len(requests)
|
|
222
|
+
for _, args := range [][]string{
|
|
223
|
+
{"get-thread", "--thread-id", "thread-a", "--team-id", "34"},
|
|
224
|
+
{"short-drama", "+submit-run", "--message", "wrong asset", "--asset-ids", "asset-a"},
|
|
225
|
+
{"get-credit-balance", "--team-id", "-1"},
|
|
226
|
+
{"space", "use"},
|
|
227
|
+
{"space", "use", "12", "34"},
|
|
228
|
+
{"space", "current"},
|
|
229
|
+
} {
|
|
230
|
+
if _, err := execute(args...); err == nil {
|
|
231
|
+
t.Errorf("accepted %v", args)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if len(requests) != before {
|
|
235
|
+
t.Fatal("invalid operations reached API")
|
|
236
|
+
}
|
|
237
|
+
if _, err := execute("space", "use", "99"); err == nil {
|
|
238
|
+
t.Fatal("nonmember selection saved")
|
|
239
|
+
}
|
|
240
|
+
listFailure = true
|
|
241
|
+
if _, err := execute("space", "use", "12"); err == nil {
|
|
242
|
+
t.Fatal("list failure ignored")
|
|
243
|
+
}
|
|
244
|
+
if selected, err := store.Current(); err != nil || selected.TeamID != "34" {
|
|
245
|
+
t.Fatalf("failed selection changed default: %+v, %v", selected, err)
|
|
246
|
+
}
|
|
247
|
+
before = len(requests)
|
|
248
|
+
must("space", "use", "0")
|
|
249
|
+
listTeamID = ""
|
|
250
|
+
if len(requests) != before {
|
|
251
|
+
t.Fatal("personal switch depends on team list")
|
|
252
|
+
}
|
|
253
|
+
if selected, err := store.Current(); err != nil || selected.TeamID != "0" {
|
|
254
|
+
t.Fatalf("personal selection not saved: %+v, %v", selected, err)
|
|
255
|
+
}
|
|
256
|
+
listFailure = false
|
|
257
|
+
checkList("0")
|
|
258
|
+
before = len(requests)
|
|
259
|
+
// Marketing preview resolves an existing thread before reading the new default.
|
|
260
|
+
if err := store.SaveDefault(space.Selection{TeamID: "34", Name: "B"}); err != nil {
|
|
261
|
+
t.Fatal(err)
|
|
262
|
+
}
|
|
263
|
+
requestFile := filepath.Join(t.TempDir(), "request.json")
|
|
264
|
+
if err := os.WriteFile(requestFile, []byte(`{"thread_id":"thread-a","message":"continue","general_agent_settings":{"video_model":"model"}}`), 0600); err != nil {
|
|
265
|
+
t.Fatal(err)
|
|
266
|
+
}
|
|
267
|
+
loadsBefore := credentials.loads
|
|
268
|
+
if out := must("marketing", "generate", "--request", requestFile); !strings.Contains(out, `"team_id":"12"`) {
|
|
269
|
+
t.Fatal(out)
|
|
270
|
+
}
|
|
271
|
+
if credentials.loads-loadsBefore != 2 {
|
|
272
|
+
t.Fatal("preview must read identity at initialization and check it once")
|
|
273
|
+
}
|
|
274
|
+
if len(requests) != before {
|
|
275
|
+
t.Fatal("preview contacted API")
|
|
276
|
+
}
|
|
277
|
+
if err := os.WriteFile(requestFile, []byte(`{"message":"new marketing task","general_agent_settings":{"video_model":"model"}}`), 0600); err != nil {
|
|
278
|
+
t.Fatal(err)
|
|
279
|
+
}
|
|
280
|
+
loadsBefore = credentials.loads
|
|
281
|
+
if out := must("marketing", "generate", "--request", requestFile, "--execute"); !strings.Contains(out, `"team_id":"34"`) || !strings.Contains(out, `"space_context"`) {
|
|
282
|
+
t.Fatal(out)
|
|
283
|
+
}
|
|
284
|
+
if credentials.loads-loadsBefore != 2 {
|
|
285
|
+
t.Fatal("submission must read identity at initialization and check it only at the HTTP boundary")
|
|
286
|
+
}
|
|
287
|
+
if requests[len(requests)-1] != marketingPaths["generate"]+":34" {
|
|
288
|
+
t.Fatal("independent marketing client lost scope", requests)
|
|
289
|
+
}
|
|
290
|
+
if err := store.SaveDefault(space.Selection{TeamID: "12", Name: "A"}); err != nil {
|
|
291
|
+
t.Fatal(err)
|
|
292
|
+
}
|
|
293
|
+
must("marketing", "query", "--thread-id", "marketing-thread", "--run-id", "marketing-run")
|
|
294
|
+
if requests[len(requests)-1] != marketingPaths["query"]+":34" {
|
|
295
|
+
t.Fatal("marketing task lost scope", requests)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
type spaceExternalAuthAdapter struct{ adapter.Default }
|
|
300
|
+
|
|
301
|
+
func (spaceExternalAuthAdapter) CLIOptions() adapter.CLIOptions {
|
|
302
|
+
options := adapter.Default{}.CLIOptions()
|
|
303
|
+
options.LocalCredentialsEnabled = false
|
|
304
|
+
return options
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
func (spaceExternalAuthAdapter) PrepareAPIRequest(_ context.Context, req *http.Request) (adapter.AuthDecision, error) {
|
|
308
|
+
req.Header.Set("Authorization", "Bearer adapter-key")
|
|
309
|
+
return adapter.AuthProvided, nil
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
type spaceUnavailableAccount struct{ common.AuthManager }
|
|
313
|
+
|
|
314
|
+
func (spaceUnavailableAccount) SpaceAccount(context.Context) (string, error) {
|
|
315
|
+
return "", errors.New("credential metadata unavailable")
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
func TestSpaceUsesAvailableIdentityInsteadOfLocalLoginSwitch(t *testing.T) {
|
|
319
|
+
var requests []string
|
|
320
|
+
denied := false
|
|
321
|
+
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
322
|
+
if r.Header.Get("Authorization") != "Bearer adapter-key" {
|
|
323
|
+
t.Error("adapter authentication was not used")
|
|
324
|
+
}
|
|
325
|
+
requests = append(requests, r.URL.Path+":"+r.Header.Get("X-Team-Id"))
|
|
326
|
+
switch r.URL.Path {
|
|
327
|
+
case config.GetLiteTeamListPath:
|
|
328
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"team_info_list":[{"team_id":"12","team_name":"A"}],"has_more":false}}`))
|
|
329
|
+
case config.GetCreditBalancePath:
|
|
330
|
+
if denied {
|
|
331
|
+
_, _ = w.Write([]byte(`{"ret":"101","errmsg":"denied"}`))
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"total_remain_amount":"100"}}`))
|
|
335
|
+
case config.UploadFilePath:
|
|
336
|
+
_, _ = io.Copy(io.Discard, r.Body)
|
|
337
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"pippit_asset_id":"asset-a"}}`))
|
|
338
|
+
case config.SubmitRunPath:
|
|
339
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"run":{"thread_id":"thread-a","run_id":"run-a"}}}`))
|
|
340
|
+
case config.GetThreadPath:
|
|
341
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"readable_text":"task result"}}`))
|
|
342
|
+
case config.QueryGenerateVideoResultPath:
|
|
343
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"thread_id":"thread-a","run_id":"run-a","run_state":2}}`))
|
|
344
|
+
default:
|
|
345
|
+
t.Errorf("unexpected request: %s", r.URL.Path)
|
|
346
|
+
}
|
|
347
|
+
}))
|
|
348
|
+
defer api.Close()
|
|
349
|
+
cfg := config.Load()
|
|
350
|
+
// This AK is not used by the adapter and must not supply a false identity.
|
|
351
|
+
cfg.BaseURL, cfg.AccessKey = api.URL, "unrelated-environment-key"
|
|
352
|
+
policy := spaceExternalAuthAdapter{}
|
|
353
|
+
runner := newRootRunnerWithAdapter(cfg, policy)
|
|
354
|
+
dir := filepath.Join(t.TempDir(), "space-records")
|
|
355
|
+
runner.SpaceStore = &space.Store{Dir: dir}
|
|
356
|
+
execute := func(args ...string) (string, error) {
|
|
357
|
+
var out bytes.Buffer
|
|
358
|
+
root := newRootCommandWithAdapter(&out, io.Discard, runner, policy)
|
|
359
|
+
root.SetArgs(args)
|
|
360
|
+
err := root.Execute()
|
|
361
|
+
return out.String(), err
|
|
362
|
+
}
|
|
363
|
+
out, err := execute("space", "list")
|
|
364
|
+
if err != nil || !strings.Contains(out, `"team_id":"12"`) || !strings.Contains(out, `"current":null`) {
|
|
365
|
+
t.Fatalf("space list without local identity = %s, %v", out, err)
|
|
366
|
+
}
|
|
367
|
+
if requests[len(requests)-1] != config.GetLiteTeamListPath+":" {
|
|
368
|
+
t.Fatal("team list used a selected team", requests)
|
|
369
|
+
}
|
|
370
|
+
before := len(requests)
|
|
371
|
+
for _, args := range [][]string{
|
|
372
|
+
{"space", "use", "0"}, {"space", "use", "12"},
|
|
373
|
+
} {
|
|
374
|
+
if _, err := execute(args...); err == nil || !strings.Contains(err.Error(), "无法识别当前账号") {
|
|
375
|
+
t.Fatalf("%v: %v", args, err)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if len(requests) != before {
|
|
379
|
+
t.Fatal("unknown identity tried to persist a default space")
|
|
380
|
+
}
|
|
381
|
+
for _, teamID := range []string{"0", "12", "34"} {
|
|
382
|
+
if _, err := execute("get-credit-balance", "--team-id", teamID); err != nil {
|
|
383
|
+
t.Fatal(err)
|
|
384
|
+
}
|
|
385
|
+
wantHeader := teamID
|
|
386
|
+
if teamID == "0" {
|
|
387
|
+
wantHeader = ""
|
|
388
|
+
}
|
|
389
|
+
if requests[len(requests)-1] != config.GetCreditBalancePath+":"+wantHeader {
|
|
390
|
+
t.Fatal("explicit space was not forwarded", requests)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
file := filepath.Join(t.TempDir(), "image.png")
|
|
394
|
+
if err := os.WriteFile(file, []byte("image"), 0600); err != nil {
|
|
395
|
+
t.Fatal(err)
|
|
396
|
+
}
|
|
397
|
+
for _, tc := range []struct {
|
|
398
|
+
args []string
|
|
399
|
+
path string
|
|
400
|
+
}{
|
|
401
|
+
{[]string{"upload-file", "--path", file}, config.UploadFilePath},
|
|
402
|
+
{[]string{"short-drama", "+submit-run", "--message", "test", "--asset-ids", "asset-a"}, config.SubmitRunPath},
|
|
403
|
+
{[]string{"get-thread", "--thread-id", "thread-a"}, config.GetThreadPath},
|
|
404
|
+
{[]string{"marketing", "query", "--thread-id", "thread-a", "--run-id", "run-a"}, config.QueryGenerateVideoResultPath},
|
|
405
|
+
} {
|
|
406
|
+
if _, err := execute(append(tc.args, "--team-id", "12")...); err != nil {
|
|
407
|
+
t.Fatalf("%v: %v", tc.args, err)
|
|
408
|
+
}
|
|
409
|
+
if requests[len(requests)-1] != tc.path+":12" {
|
|
410
|
+
t.Fatal("explicit task space was not forwarded", requests)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
before = len(requests)
|
|
414
|
+
denied = true
|
|
415
|
+
if _, err := execute("get-credit-balance", "--team-id", "12"); err == nil || !strings.Contains(err.Error(), "denied") {
|
|
416
|
+
t.Fatalf("server denial was not returned: %v", err)
|
|
417
|
+
}
|
|
418
|
+
denied = false
|
|
419
|
+
if len(requests) != before+1 || requests[len(requests)-1] != config.GetCreditBalancePath+":12" {
|
|
420
|
+
t.Fatal("server denial retried or switched spaces", requests)
|
|
421
|
+
}
|
|
422
|
+
if _, err := os.Stat(dir); !os.IsNotExist(err) {
|
|
423
|
+
t.Fatalf("unknown identity created local space records: %v", err)
|
|
424
|
+
}
|
|
425
|
+
if _, err := execute("get-credit-balance"); err != nil {
|
|
426
|
+
t.Fatal("ordinary adapter requests must remain available", err)
|
|
427
|
+
}
|
|
428
|
+
// Metadata lookup failure must not block an explicitly scoped API request.
|
|
429
|
+
cfg.AccessKey = ""
|
|
430
|
+
runner.Auth = spaceUnavailableAccount{}
|
|
431
|
+
if _, err := execute("get-credit-balance", "--team-id", "12"); err != nil {
|
|
432
|
+
t.Fatal("local metadata failure blocked adapter authentication", err)
|
|
433
|
+
}
|
|
434
|
+
if requests[len(requests)-1] != config.GetCreditBalancePath+":12" {
|
|
435
|
+
t.Fatal("metadata failure dropped explicit space", requests)
|
|
436
|
+
}
|
|
437
|
+
if _, err := execute("get-credit-balance"); err == nil {
|
|
438
|
+
t.Fatal("implicit space silently ignored metadata failure")
|
|
439
|
+
}
|
|
440
|
+
// With a known matching account, the login-command switch must not suppress
|
|
441
|
+
// saved defaults or their use by business requests.
|
|
442
|
+
cfg.AccessKey = "adapter-key"
|
|
443
|
+
runner.Auth = auth.NewManager(cfg, auth.WithCredentialStore(&marketingCredentialStore{credential: &auth.Credential{UID: "adapter-user", AccessKey: "adapter-key"}}))
|
|
444
|
+
if _, err := execute("space", "use", "12"); err != nil {
|
|
445
|
+
t.Fatal(err)
|
|
446
|
+
}
|
|
447
|
+
out, err = execute("space", "list")
|
|
448
|
+
if err != nil {
|
|
449
|
+
t.Fatal(err)
|
|
450
|
+
}
|
|
451
|
+
var result struct {
|
|
452
|
+
Current *space.Selection `json:"current"`
|
|
453
|
+
}
|
|
454
|
+
if err := json.Unmarshal([]byte(out), &result); err != nil || result.Current == nil || result.Current.TeamID != "12" {
|
|
455
|
+
t.Fatalf("space list = %s, %v", out, err)
|
|
456
|
+
}
|
|
457
|
+
if _, err := execute("get-credit-balance"); err != nil {
|
|
458
|
+
t.Fatal(err)
|
|
459
|
+
}
|
|
460
|
+
if requests[len(requests)-1] != config.GetCreditBalancePath+":12" {
|
|
461
|
+
t.Fatal("saved default was ignored", requests)
|
|
462
|
+
}
|
|
463
|
+
if _, err := execute("get-credit-balance", "--team-id", "0"); err != nil {
|
|
464
|
+
t.Fatal(err)
|
|
465
|
+
}
|
|
466
|
+
if requests[len(requests)-1] != config.GetCreditBalancePath+":" {
|
|
467
|
+
t.Fatal("personal override was ignored", requests)
|
|
468
|
+
}
|
|
469
|
+
}
|
package/cmd/upload_file_test.go
CHANGED
|
@@ -2,6 +2,7 @@ package cmd
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"bytes"
|
|
5
|
+
"encoding/json"
|
|
5
6
|
"io"
|
|
6
7
|
"net/http"
|
|
7
8
|
"net/http/httptest"
|
|
@@ -77,7 +78,8 @@ func TestUploadFileMediaContract(t *testing.T) {
|
|
|
77
78
|
if err := root.Execute(); err != nil {
|
|
78
79
|
t.Fatal(err)
|
|
79
80
|
}
|
|
80
|
-
|
|
81
|
+
var output map[string]string
|
|
82
|
+
if json.Unmarshal(stdout.Bytes(), &output) != nil || output["asset_id"] != "asset_original" || output["team_id"] != "0" || calls != 1 {
|
|
81
83
|
t.Fatalf("stdout = %q, requests = %d", stdout.String(), calls)
|
|
82
84
|
}
|
|
83
85
|
})
|
package/dist/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
e7ee32acef1c0a16912c46500ae7f15c46d920953792d1012886fac7e6424b2e pippit-tool-cli-1.0.43-darwin-amd64.tar.gz
|
|
2
|
+
57e4df86eaa244cd9845eb966299c8c2e1186f74495da0c16f0b378fb4bc5125 pippit-tool-cli-1.0.43-darwin-arm64.tar.gz
|
|
3
|
+
21823c7a675ebcaadebe2ddbfa0324b9e939220d4dd74d5d5d4546f5e6938060 pippit-tool-cli-1.0.43-linux-amd64.tar.gz
|
|
4
|
+
e387bc8eaeafa430a9a594ed1cc381964953b598875b520a6d009a12aec9d619 pippit-tool-cli-1.0.43-linux-arm64.tar.gz
|
|
5
|
+
4569d389a20c75f0485ecc24ad5034308a818f294886dabdf07b2804e915f3a2 pippit-tool-cli-1.0.43-windows-amd64.zip
|
|
6
|
+
078fb4fb09353a58095e1370668884664aad1760ae065983bb23abfdcd9939d1 pippit-tool-cli-1.0.43-windows-arm64.zip
|
|
@@ -12,22 +12,26 @@ func TestConfigureDefaultAllowRootCommands(t *testing.T) {
|
|
|
12
12
|
for _, tc := range []struct {
|
|
13
13
|
name string
|
|
14
14
|
defaultAllow bool
|
|
15
|
+
localAuth bool
|
|
15
16
|
allowlist []string
|
|
16
17
|
want []string
|
|
17
18
|
}{
|
|
18
19
|
{name: "默认拒绝且 nil 白名单"},
|
|
19
20
|
{name: "默认拒绝且空白名单", allowlist: []string{}},
|
|
20
21
|
{name: "默认拒绝且有白名单", allowlist: []string{"canvas"}, want: []string{"canvas"}},
|
|
21
|
-
{name: "默认放行且 nil 白名单", defaultAllow: true, want: []string{"canvas", "future-command"}},
|
|
22
|
-
{name: "默认放行且空白名单", defaultAllow: true, allowlist: []string{}, want: []string{"canvas", "future-command"}},
|
|
23
|
-
{name: "默认放行且有白名单", defaultAllow: true, allowlist: []string{"canvas"}, want: []string{"canvas", "future-command"}},
|
|
22
|
+
{name: "默认放行且 nil 白名单", defaultAllow: true, want: []string{"canvas", "future-command", "space"}},
|
|
23
|
+
{name: "默认放行且空白名单", defaultAllow: true, allowlist: []string{}, want: []string{"canvas", "future-command", "space"}},
|
|
24
|
+
{name: "默认放行且有白名单", defaultAllow: true, allowlist: []string{"canvas"}, want: []string{"canvas", "future-command", "space"}},
|
|
25
|
+
{name: "启用本地账号", defaultAllow: true, localAuth: true, want: []string{"canvas", "future-command", "login", "logout", "space", "status"}},
|
|
26
|
+
{name: "启用本地账号仍受白名单限制", localAuth: true, allowlist: []string{"space"}, want: []string{"space"}},
|
|
27
|
+
{name: "关闭本地登录仍允许空间命令", allowlist: []string{"login", "logout", "space", "status"}, want: []string{"space"}},
|
|
24
28
|
} {
|
|
25
29
|
t.Run(tc.name, func(t *testing.T) {
|
|
26
30
|
root := &cobra.Command{Use: "tool"}
|
|
27
|
-
for _, name := range []string{"canvas", "future-command", "login", "logout", "status", "update"} {
|
|
31
|
+
for _, name := range []string{"canvas", "future-command", "login", "logout", "space", "status", "update"} {
|
|
28
32
|
root.AddCommand(&cobra.Command{Use: name})
|
|
29
33
|
}
|
|
30
|
-
Configure(root, adapter.CLIOptions{DefaultAllowRootCommands: tc.defaultAllow, AllowedRootCommands: tc.allowlist})
|
|
34
|
+
Configure(root, adapter.CLIOptions{DefaultAllowRootCommands: tc.defaultAllow, AllowedRootCommands: tc.allowlist, LocalCredentialsEnabled: tc.localAuth})
|
|
31
35
|
var got []string
|
|
32
36
|
for _, child := range root.Commands() {
|
|
33
37
|
got = append(got, child.Name())
|
|
@@ -5,7 +5,6 @@ import (
|
|
|
5
5
|
"encoding/base64"
|
|
6
6
|
"encoding/json"
|
|
7
7
|
"errors"
|
|
8
|
-
"fmt"
|
|
9
8
|
"io"
|
|
10
9
|
"mime"
|
|
11
10
|
"net"
|
|
@@ -14,6 +13,8 @@ import (
|
|
|
14
13
|
"strings"
|
|
15
14
|
"sync"
|
|
16
15
|
"time"
|
|
16
|
+
|
|
17
|
+
"github.com/Pippit-dev/pippit-cli/internal/version"
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
const maxCallbackBodyBytes = 64 << 10
|
|
@@ -37,7 +38,6 @@ type browserFlow struct {
|
|
|
37
38
|
secret string
|
|
38
39
|
state string
|
|
39
40
|
source string
|
|
40
|
-
origin string
|
|
41
41
|
listener net.Listener
|
|
42
42
|
server *http.Server
|
|
43
43
|
payload chan accessKeyPayload
|
|
@@ -93,6 +93,9 @@ func startBrowserFlow(authBaseURL *url.URL, randomReader io.Reader, deviceID, to
|
|
|
93
93
|
query.Set("callback", callback.String())
|
|
94
94
|
query.Set("random_secret_key", secret)
|
|
95
95
|
query.Set("source", loginSource)
|
|
96
|
+
query.Set("cli_version", version.BuildVersion())
|
|
97
|
+
// Protocol 1 supports callbacks from both official login origins.
|
|
98
|
+
query.Set("auth_protocol", "1")
|
|
96
99
|
query.Set("device_id", deviceID)
|
|
97
100
|
if tokenID != "" {
|
|
98
101
|
query.Set("token_id", tokenID)
|
|
@@ -111,7 +114,6 @@ func startBrowserFlow(authBaseURL *url.URL, randomReader io.Reader, deviceID, to
|
|
|
111
114
|
secret: secret,
|
|
112
115
|
state: state,
|
|
113
116
|
source: loginSource,
|
|
114
|
-
origin: originOf(authBaseURL),
|
|
115
117
|
listener: listener,
|
|
116
118
|
payload: make(chan accessKeyPayload, 1),
|
|
117
119
|
serveErr: make(chan error, 1),
|
|
@@ -166,11 +168,12 @@ func (f *browserFlow) handleCallback(writer http.ResponseWriter, request *http.R
|
|
|
166
168
|
http.Error(writer, "invalid callback target", http.StatusBadRequest)
|
|
167
169
|
return
|
|
168
170
|
}
|
|
169
|
-
|
|
171
|
+
origin := request.Header.Get("Origin")
|
|
172
|
+
if !isAllowedAuthOrigin(origin) {
|
|
170
173
|
http.Error(writer, "origin not allowed", http.StatusForbidden)
|
|
171
174
|
return
|
|
172
175
|
}
|
|
173
|
-
f.setCORSHeaders(writer.Header())
|
|
176
|
+
f.setCORSHeaders(writer.Header(), origin)
|
|
174
177
|
|
|
175
178
|
if request.Method == http.MethodOptions {
|
|
176
179
|
if !strings.EqualFold(strings.TrimSpace(request.Header.Get("Access-Control-Request-Method")), http.MethodPost) ||
|
|
@@ -288,8 +291,14 @@ func (f *browserFlow) callbackURLHost() string {
|
|
|
288
291
|
return parsed.Host
|
|
289
292
|
}
|
|
290
293
|
|
|
291
|
-
|
|
292
|
-
|
|
294
|
+
// Authentication trusts only these official HTTPS origins, independently of
|
|
295
|
+
// runtime business API configuration or browser-supplied query parameters.
|
|
296
|
+
func isAllowedAuthOrigin(origin string) bool {
|
|
297
|
+
return origin == "https://xyq.jianying.com" || origin == "https://www.xiaoyunque.com"
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
func (f *browserFlow) setCORSHeaders(header http.Header, origin string) {
|
|
301
|
+
header.Set("Access-Control-Allow-Origin", origin)
|
|
293
302
|
header.Set("Access-Control-Allow-Methods", "POST")
|
|
294
303
|
header.Set("Access-Control-Allow-Headers", "Content-Type")
|
|
295
304
|
header.Set("Access-Control-Allow-Private-Network", "true")
|
|
@@ -318,7 +327,3 @@ func ensureJSONEOF(decoder *json.Decoder) error {
|
|
|
318
327
|
}
|
|
319
328
|
return err
|
|
320
329
|
}
|
|
321
|
-
|
|
322
|
-
func originOf(value *url.URL) string {
|
|
323
|
-
return fmt.Sprintf("%s://%s", strings.ToLower(value.Scheme), value.Host)
|
|
324
|
-
}
|