@pippit-dev/cli 1.0.42 → 1.0.44
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 +24 -1
- 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 +13 -5
- 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/auth_test.go +21 -17
- package/internal/auth/loopback_origins_test.go +1 -1
- package/internal/auth/manager.go +1 -1
- 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 +16 -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/package.json +1 -1
- package/scripts/canvas-command.js +37 -4
- package/scripts/install-wizard.js +1 -1
- package/scripts/run.js +2 -2
- package/scripts/telemetry.js +3 -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 +42 -18
- 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
package/dist/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
8bf338b7fc4bb3de289778896059f364f91743e7fe3127380e193f29bb10fecf pippit-tool-cli-1.0.44-darwin-amd64.tar.gz
|
|
2
|
+
fd7aac74fda167481b76e3a7aa2f30e45271670cad11f0e969080db1b135411a pippit-tool-cli-1.0.44-darwin-arm64.tar.gz
|
|
3
|
+
526e3641047d7e997fb5c271d795337692b0a47c092622747f6e2b4e9d22e473 pippit-tool-cli-1.0.44-linux-amd64.tar.gz
|
|
4
|
+
6170436a3ec60dc2a2681e6f790c6a52d9daa4248ce8c3eede83136a62ccb4b6 pippit-tool-cli-1.0.44-linux-arm64.tar.gz
|
|
5
|
+
d3c7166314bb52deae5e0caf383ac7e3cdde27c61325d6556de43f8f46aa9963 pippit-tool-cli-1.0.44-windows-amd64.zip
|
|
6
|
+
9cb3d470688cd89a42a2267d9d181159dd624937c9590d3b744db25a66edbac9 pippit-tool-cli-1.0.44-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())
|
|
@@ -96,7 +96,7 @@ func (s *memoryCredentialStore) Delete(ctx context.Context) error {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
func TestBrowserFlowUsesDedicatedPageAndStrictBoundCallback(t *testing.T) {
|
|
99
|
-
authURL, _ := url.Parse(config.
|
|
99
|
+
authURL, _ := url.Parse(config.DefaultAuthBaseURL)
|
|
100
100
|
deviceID, _ := randomEncoded(bytes.NewReader(bytes.Repeat([]byte{0x21}, deviceIDBytes)), deviceIDBytes)
|
|
101
101
|
flow, err := startBrowserFlow(
|
|
102
102
|
authURL, bytes.NewReader(bytes.Repeat([]byte{0x42}, 2*randomBindingBytes)),
|
|
@@ -111,7 +111,7 @@ func TestBrowserFlowUsesDedicatedPageAndStrictBoundCallback(t *testing.T) {
|
|
|
111
111
|
if err != nil {
|
|
112
112
|
t.Fatal(err)
|
|
113
113
|
}
|
|
114
|
-
if loginURL.Scheme+"://"+loginURL.Host != config.
|
|
114
|
+
if loginURL.Scheme+"://"+loginURL.Host != config.DefaultAuthBaseURL || loginURL.Path != loginPagePath {
|
|
115
115
|
t.Fatalf("login URL target = %s://%s%s", loginURL.Scheme, loginURL.Host, loginURL.Path)
|
|
116
116
|
}
|
|
117
117
|
query := loginURL.Query()
|
|
@@ -152,30 +152,30 @@ func TestBrowserFlowUsesDedicatedPageAndStrictBoundCallback(t *testing.T) {
|
|
|
152
152
|
}
|
|
153
153
|
wrongSecret := payload
|
|
154
154
|
wrongSecret.RandomSecretKey = "wrong-secret"
|
|
155
|
-
if status := sendCallback(t, callbackURL, config.
|
|
155
|
+
if status := sendCallback(t, callbackURL, config.DefaultAuthBaseURL, wrongSecret); status != http.StatusBadRequest {
|
|
156
156
|
t.Fatalf("wrong secret status = %d", status)
|
|
157
157
|
}
|
|
158
|
-
if status := postRawCallback(t, callbackURL, config.
|
|
158
|
+
if status := postRawCallback(t, callbackURL, config.DefaultAuthBaseURL, `{"type":"access_key","unknown":true}`); status != http.StatusBadRequest {
|
|
159
159
|
t.Fatalf("unknown field status = %d", status)
|
|
160
160
|
}
|
|
161
|
-
if status := sendCallback(t, callbackURL+"&state=duplicate", config.
|
|
161
|
+
if status := sendCallback(t, callbackURL+"&state=duplicate", config.DefaultAuthBaseURL, payload); status != http.StatusBadRequest {
|
|
162
162
|
t.Fatalf("duplicate state status = %d", status)
|
|
163
163
|
}
|
|
164
|
-
if status := sendCallback(t, callbackURL+"&unexpected=value", config.
|
|
164
|
+
if status := sendCallback(t, callbackURL+"&unexpected=value", config.DefaultAuthBaseURL, payload); status != http.StatusBadRequest {
|
|
165
165
|
t.Fatalf("unexpected transport query status = %d", status)
|
|
166
166
|
}
|
|
167
|
-
if status := sendPreflight(t, callbackURL, config.
|
|
167
|
+
if status := sendPreflight(t, callbackURL, config.DefaultAuthBaseURL); status != http.StatusNoContent {
|
|
168
168
|
t.Fatalf("preflight status = %d", status)
|
|
169
169
|
}
|
|
170
170
|
securityRuntimeCallback := callbackURL + "&a_bogus=signed-request&msToken=transport-token"
|
|
171
|
-
if status := sendCallback(t, securityRuntimeCallback, config.
|
|
171
|
+
if status := sendCallback(t, securityRuntimeCallback, config.DefaultAuthBaseURL, payload); status != http.StatusOK {
|
|
172
172
|
t.Fatalf("security-runtime callback status = %d", status)
|
|
173
173
|
}
|
|
174
174
|
got, err := flow.wait(context.Background())
|
|
175
175
|
if err != nil || got.AccessKey != payload.AccessKey || got.UID != payload.UID || got.TokenID != payload.TokenID {
|
|
176
176
|
t.Fatalf("callback payload = %#v, %v", credentialPayloadWithoutSecret(got), err)
|
|
177
177
|
}
|
|
178
|
-
if status := sendCallback(t, callbackURL, config.
|
|
178
|
+
if status := sendCallback(t, callbackURL, config.DefaultAuthBaseURL, payload); status != http.StatusConflict {
|
|
179
179
|
t.Fatalf("replayed callback status = %d", status)
|
|
180
180
|
}
|
|
181
181
|
}
|
|
@@ -185,6 +185,7 @@ func TestManagerLoginStoresPageIssuedCredentialWithoutServerExchange(t *testing.
|
|
|
185
185
|
store := &memoryCredentialStore{}
|
|
186
186
|
cfg := config.Load()
|
|
187
187
|
cfg.AccessKey = ""
|
|
188
|
+
cfg.BaseURL = "https://untrusted.invalid"
|
|
188
189
|
manager := NewManager(
|
|
189
190
|
cfg,
|
|
190
191
|
WithCredentialStore(store),
|
|
@@ -204,6 +205,9 @@ func TestManagerLoginStoresPageIssuedCredentialWithoutServerExchange(t *testing.
|
|
|
204
205
|
if parseErr != nil {
|
|
205
206
|
return parseErr
|
|
206
207
|
}
|
|
208
|
+
if loginURL.Scheme != "https" || loginURL.Host != "www.xiaoyunque.com" {
|
|
209
|
+
t.Errorf("login origin = %s://%s, want https://www.xiaoyunque.com", loginURL.Scheme, loginURL.Host)
|
|
210
|
+
}
|
|
207
211
|
if loginURL.Path != loginPagePath || loginURL.Query().Get("force") != "" {
|
|
208
212
|
return fmt.Errorf("unexpected login URL")
|
|
209
213
|
}
|
|
@@ -218,7 +222,7 @@ func TestManagerLoginStoresPageIssuedCredentialWithoutServerExchange(t *testing.
|
|
|
218
222
|
Source: loginSource,
|
|
219
223
|
CallbackURL: loginURL.Query().Get("callback"),
|
|
220
224
|
}
|
|
221
|
-
if status := sendCallback(t, payload.CallbackURL,
|
|
225
|
+
if status := sendCallback(t, payload.CallbackURL, loginURL.Scheme+"://"+loginURL.Host, payload); status != http.StatusOK {
|
|
222
226
|
return fmt.Errorf("callback status %d", status)
|
|
223
227
|
}
|
|
224
228
|
return nil
|
|
@@ -273,7 +277,7 @@ func TestManagerWaitsForCallbackWhenBrowserOpenFails(t *testing.T) {
|
|
|
273
277
|
Source: loginSource,
|
|
274
278
|
CallbackURL: loginURL.Query().Get("callback"),
|
|
275
279
|
}
|
|
276
|
-
if status := sendCallback(t, payload.CallbackURL, config.
|
|
280
|
+
if status := sendCallback(t, payload.CallbackURL, config.DefaultAuthBaseURL, payload); status != http.StatusOK {
|
|
277
281
|
return fmt.Errorf("callback status %d", status)
|
|
278
282
|
}
|
|
279
283
|
return errors.New("browser opener unavailable")
|
|
@@ -347,7 +351,7 @@ func TestManagerReauthenticationPinsAccountAndRequiresRotation(t *testing.T) {
|
|
|
347
351
|
}
|
|
348
352
|
|
|
349
353
|
func TestBrowserFlowRequiresExpectedAccountForForceAndNeverUsesRawUID(t *testing.T) {
|
|
350
|
-
authURL, _ := url.Parse(config.
|
|
354
|
+
authURL, _ := url.Parse(config.DefaultAuthBaseURL)
|
|
351
355
|
deviceID, _ := randomEncoded(bytes.NewReader(bytes.Repeat([]byte{0x22}, deviceIDBytes)), deviceIDBytes)
|
|
352
356
|
if _, err := startBrowserFlow(
|
|
353
357
|
authURL, bytes.NewReader(bytes.Repeat([]byte{0x42}, 2*randomBindingBytes)),
|
|
@@ -378,7 +382,7 @@ func TestBrowserFlowRequiresExpectedAccountForForceAndNeverUsesRawUID(t *testing
|
|
|
378
382
|
}
|
|
379
383
|
|
|
380
384
|
func TestBrowserFlowSendsStoredTokenIDOnNormalLogin(t *testing.T) {
|
|
381
|
-
authURL, _ := url.Parse(config.
|
|
385
|
+
authURL, _ := url.Parse(config.DefaultAuthBaseURL)
|
|
382
386
|
deviceID, _ := randomEncoded(bytes.NewReader(bytes.Repeat([]byte{0x23}, deviceIDBytes)), deviceIDBytes)
|
|
383
387
|
flow, err := startBrowserFlow(
|
|
384
388
|
authURL, bytes.NewReader(bytes.Repeat([]byte{0x44}, 2*randomBindingBytes)),
|
|
@@ -398,7 +402,7 @@ func TestBrowserFlowSendsStoredTokenIDOnNormalLogin(t *testing.T) {
|
|
|
398
402
|
}
|
|
399
403
|
|
|
400
404
|
func TestBrowserFlowAllowsLogoutIdentityTokenWithoutAccountBinding(t *testing.T) {
|
|
401
|
-
authURL, _ := url.Parse(config.
|
|
405
|
+
authURL, _ := url.Parse(config.DefaultAuthBaseURL)
|
|
402
406
|
deviceID, _ := randomEncoded(bytes.NewReader(bytes.Repeat([]byte{0x24}, deviceIDBytes)), deviceIDBytes)
|
|
403
407
|
flow, err := startBrowserFlow(
|
|
404
408
|
authURL, bytes.NewReader(bytes.Repeat([]byte{0x45}, 2*randomBindingBytes)),
|
|
@@ -464,7 +468,7 @@ func TestManagerDerivesExpectedAccountFromDurableScope(t *testing.T) {
|
|
|
464
468
|
Source: loginSource,
|
|
465
469
|
CallbackURL: loginURL.Query().Get("callback"),
|
|
466
470
|
}
|
|
467
|
-
if status := sendCallback(t, payload.CallbackURL, config.
|
|
471
|
+
if status := sendCallback(t, payload.CallbackURL, config.DefaultAuthBaseURL, payload); status != http.StatusOK {
|
|
468
472
|
return fmt.Errorf("callback status %d", status)
|
|
469
473
|
}
|
|
470
474
|
return nil
|
|
@@ -488,7 +492,7 @@ func TestResolveStatusLogoutAndAuthOrigin(t *testing.T) {
|
|
|
488
492
|
cfg.AccessKey = " env-ak "
|
|
489
493
|
cfg.BaseURL = "https://untrusted.invalid"
|
|
490
494
|
manager := NewManager(cfg, WithCredentialStore(store), withClockForTest(func() time.Time { return fixedNow }))
|
|
491
|
-
if manager.authBaseURL.String() != config.
|
|
495
|
+
if manager.authBaseURL.String() != config.DefaultAuthBaseURL {
|
|
492
496
|
t.Fatalf("browser login followed runtime business base URL: %q", manager.authBaseURL)
|
|
493
497
|
}
|
|
494
498
|
if key, err := manager.ResolveAccessKey(context.Background()); err != nil || key != "env-ak" {
|
|
@@ -738,7 +742,7 @@ func callbackOpener(t *testing.T, expiry time.Time, uid, tokenID, accessKey stri
|
|
|
738
742
|
Source: loginSource,
|
|
739
743
|
CallbackURL: loginURL.Query().Get("callback"),
|
|
740
744
|
}
|
|
741
|
-
if status := sendCallback(t, payload.CallbackURL, config.
|
|
745
|
+
if status := sendCallback(t, payload.CallbackURL, config.DefaultAuthBaseURL, payload); status != http.StatusOK {
|
|
742
746
|
return fmt.Errorf("callback status %d", status)
|
|
743
747
|
}
|
|
744
748
|
return nil
|
|
@@ -16,7 +16,7 @@ import (
|
|
|
16
16
|
|
|
17
17
|
func newOriginTestFlow(t *testing.T) (*browserFlow, accessKeyPayload) {
|
|
18
18
|
t.Helper()
|
|
19
|
-
authURL, _ := url.Parse(config.
|
|
19
|
+
authURL, _ := url.Parse(config.DefaultAuthBaseURL)
|
|
20
20
|
deviceID, _ := randomEncoded(bytes.NewReader(bytes.Repeat([]byte{0x21}, deviceIDBytes)), deviceIDBytes)
|
|
21
21
|
flow, err := startBrowserFlow(authURL, bytes.NewReader(bytes.Repeat([]byte{0x42}, 2*randomBindingBytes)), deviceID, "", "", false)
|
|
22
22
|
if err != nil {
|
package/internal/auth/manager.go
CHANGED
|
@@ -46,7 +46,7 @@ func WithCredentialStore(store CredentialStore) ManagerOption {
|
|
|
46
46
|
// API configuration does not affect the browser identity origin.
|
|
47
47
|
func NewManager(cfg *config.Config, options ...ManagerOption) *Manager {
|
|
48
48
|
serviceName := config.DefaultAuthStoreServiceName
|
|
49
|
-
authBaseURL, _ := url.Parse(config.
|
|
49
|
+
authBaseURL, _ := url.Parse(config.DefaultAuthBaseURL)
|
|
50
50
|
manager := &Manager{
|
|
51
51
|
localCredentialsEnabled: true,
|
|
52
52
|
cfg: cfg,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package auth
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"errors"
|
|
6
|
+
|
|
7
|
+
"github.com/Pippit-dev/pippit-cli/internal/space"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// SpaceAccount uses the verified browser UID, stable across AK rotation.
|
|
11
|
+
func (m *Manager) SpaceAccount(ctx context.Context) (string, error) {
|
|
12
|
+
// Re-read at request boundaries so an account switch in another process
|
|
13
|
+
// cannot keep using this process's cached identity or credential.
|
|
14
|
+
m.clearCredentialCache()
|
|
15
|
+
credential, err := m.loadCredential(ctx)
|
|
16
|
+
if errors.Is(err, ErrCredentialNotFound) {
|
|
17
|
+
return "", nil
|
|
18
|
+
}
|
|
19
|
+
if err != nil {
|
|
20
|
+
return "", err
|
|
21
|
+
}
|
|
22
|
+
if credential.UID == "" || credential.AccessKey == "" {
|
|
23
|
+
return "", nil
|
|
24
|
+
}
|
|
25
|
+
return "user:" + space.Digest(credential.UID), nil
|
|
26
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
package auth
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"strings"
|
|
6
|
+
"testing"
|
|
7
|
+
"time"
|
|
8
|
+
|
|
9
|
+
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
func TestSpaceAccountRefreshesCredentialAndSurvivesAKRotation(t *testing.T) {
|
|
13
|
+
ctx := context.Background()
|
|
14
|
+
cfg := config.Load()
|
|
15
|
+
cfg.AccessKey = ""
|
|
16
|
+
store := &memoryCredentialStore{credential: &Credential{UID: "user-a", AccessKey: "key-a", ExpiredAt: time.Now().Add(time.Hour).Unix()}}
|
|
17
|
+
manager := NewManager(cfg, WithCredentialStore(store))
|
|
18
|
+
a, err := manager.SpaceAccount(ctx)
|
|
19
|
+
if err != nil {
|
|
20
|
+
t.Fatal(err)
|
|
21
|
+
}
|
|
22
|
+
if strings.Contains(a, "user-a") || strings.Contains(a, "key-a") {
|
|
23
|
+
t.Fatal("raw identity leaked")
|
|
24
|
+
}
|
|
25
|
+
if err := store.Save(ctx, &Credential{UID: "user-a", AccessKey: "key-rotated", ExpiredAt: time.Now().Add(time.Hour).Unix()}); err != nil {
|
|
26
|
+
t.Fatal(err)
|
|
27
|
+
}
|
|
28
|
+
after, err := manager.SpaceAccount(ctx)
|
|
29
|
+
if err != nil || after != a {
|
|
30
|
+
t.Fatalf("rotation changed identity: %s, %v", after, err)
|
|
31
|
+
}
|
|
32
|
+
if key, err := manager.ResolveAccessKey(ctx); err != nil || key != "key-rotated" {
|
|
33
|
+
t.Fatalf("request used stale credential: %v", err)
|
|
34
|
+
}
|
|
35
|
+
if err := store.Save(ctx, &Credential{UID: "user-b", AccessKey: "key-b", ExpiredAt: time.Now().Add(time.Hour).Unix()}); err != nil {
|
|
36
|
+
t.Fatal(err)
|
|
37
|
+
}
|
|
38
|
+
b, err := manager.SpaceAccount(ctx)
|
|
39
|
+
if err != nil || b == a {
|
|
40
|
+
t.Fatal("new account inherited old identity", err)
|
|
41
|
+
}
|
|
42
|
+
}
|
package/internal/canvas/get.go
CHANGED
|
@@ -14,6 +14,8 @@ type GetOptions struct {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
type GetResult struct {
|
|
17
|
+
TeamID string `json:"team_id"`
|
|
18
|
+
SpaceContext string `json:"space_context,omitempty"`
|
|
17
19
|
RequestedAssetIDs []string `json:"requested_asset_ids"`
|
|
18
20
|
Assets []json.RawMessage `json:"assets"`
|
|
19
21
|
LogID string `json:"log_id,omitempty"`
|
|
@@ -77,11 +79,16 @@ func queryAssets(ctx context.Context, assetIDs []string, requireAll bool, runner
|
|
|
77
79
|
envelope.LogID,
|
|
78
80
|
)
|
|
79
81
|
}
|
|
80
|
-
|
|
82
|
+
result := &GetResult{
|
|
83
|
+
TeamID: common.SpaceTeamID(ctx),
|
|
81
84
|
RequestedAssetIDs: append([]string(nil), assetIDs...),
|
|
82
85
|
Assets: ordered,
|
|
83
86
|
LogID: strings.TrimSpace(envelope.LogID),
|
|
84
|
-
}
|
|
87
|
+
}
|
|
88
|
+
if session := common.SpaceSession(ctx); session != nil {
|
|
89
|
+
result.SpaceContext = session.Token()
|
|
90
|
+
}
|
|
91
|
+
return result, nil
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
func normalizeAssetIDs(values []string) ([]string, error) {
|
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
package commandnames
|
|
3
3
|
|
|
4
4
|
const (
|
|
5
|
-
Login
|
|
6
|
-
Logout
|
|
7
|
-
Status
|
|
8
|
-
|
|
5
|
+
Login = "login"
|
|
6
|
+
Logout = "logout"
|
|
7
|
+
Status = "status"
|
|
8
|
+
Space = "space"
|
|
9
|
+
Update = "update"
|
|
10
|
+
Completion = "completion"
|
|
11
|
+
Help = "help"
|
|
12
|
+
Model = "model"
|
|
13
|
+
DownloadResult = "download-result"
|
|
9
14
|
)
|
|
@@ -90,6 +90,11 @@ func (c *httpClient) SendRequest(ctx context.Context, path string, body any, out
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
func (c *httpClient) SendRequestWithHeaders(ctx context.Context, path string, body any, headers map[string]string, out any) error {
|
|
93
|
+
var scopeErr error
|
|
94
|
+
ctx, scopeErr = c.prepareSpaceRequest(ctx, path, body)
|
|
95
|
+
if scopeErr != nil {
|
|
96
|
+
return scopeErr
|
|
97
|
+
}
|
|
93
98
|
method := http.MethodPost
|
|
94
99
|
if body == nil {
|
|
95
100
|
method = http.MethodGet
|
|
@@ -145,6 +150,11 @@ func (c *httpClient) SendRequestWithHeaders(ctx context.Context, path string, bo
|
|
|
145
150
|
}
|
|
146
151
|
|
|
147
152
|
func (c *httpClient) SendMultipartRequest(ctx context.Context, path string, fields map[string]string, file MultipartFile, out any) error {
|
|
153
|
+
var scopeErr error
|
|
154
|
+
ctx, scopeErr = c.prepareSpaceRequest(ctx, path, nil)
|
|
155
|
+
if scopeErr != nil {
|
|
156
|
+
return scopeErr
|
|
157
|
+
}
|
|
148
158
|
reqURL, err := c.resolveURL(path, nil)
|
|
149
159
|
if err != nil {
|
|
150
160
|
return err
|
|
@@ -242,6 +252,7 @@ func (c *httpClient) prepareRequest(ctx context.Context, req *http.Request, head
|
|
|
242
252
|
c.injectHeaders(req, headers)
|
|
243
253
|
// Rebuild the authentication header only for requests to the configured API origin; general request headers cannot override it.
|
|
244
254
|
req.Header.Del("Authorization")
|
|
255
|
+
req.Header.Del("X-Team-Id")
|
|
245
256
|
if !isAPIOrigin {
|
|
246
257
|
c.adapter.StripSensitiveHeaders(req)
|
|
247
258
|
return nil
|
|
@@ -250,6 +261,16 @@ func (c *httpClient) prepareRequest(ctx context.Context, req *http.Request, head
|
|
|
250
261
|
if err != nil {
|
|
251
262
|
return err
|
|
252
263
|
}
|
|
264
|
+
// The selected space, like credentials, is rebuilt only for the API origin.
|
|
265
|
+
if session := SpaceSession(ctx); session != nil {
|
|
266
|
+
if err := session.CheckIdentity(ctx); err != nil {
|
|
267
|
+
return err
|
|
268
|
+
}
|
|
269
|
+
req.Header.Del("X-Team-Id")
|
|
270
|
+
if session.TeamID() != "0" {
|
|
271
|
+
req.Header.Set("X-Team-Id", session.TeamID())
|
|
272
|
+
}
|
|
273
|
+
}
|
|
253
274
|
switch decision {
|
|
254
275
|
case adapter.AuthProvided:
|
|
255
276
|
return nil
|
|
@@ -313,6 +334,7 @@ func (c *httpClient) checkRedirect(req *http.Request, via []*http.Request) error
|
|
|
313
334
|
}
|
|
314
335
|
if !trusted {
|
|
315
336
|
req.Header.Del("Authorization")
|
|
337
|
+
req.Header.Del("X-Team-Id")
|
|
316
338
|
c.adapter.StripSensitiveHeaders(req)
|
|
317
339
|
}
|
|
318
340
|
return nil
|
|
@@ -362,6 +384,7 @@ func (c *httpClient) do(req *http.Request, out any) error {
|
|
|
362
384
|
if err := sonic.Unmarshal(data, out); err != nil {
|
|
363
385
|
return fmt.Errorf("解析响应体失败: %w", err)
|
|
364
386
|
}
|
|
387
|
+
rememberSpaceResponse(req.Context(), data)
|
|
365
388
|
return nil
|
|
366
389
|
}
|
|
367
390
|
|
|
@@ -22,6 +22,11 @@ func detectHostSource(getenv func(string) string) string {
|
|
|
22
22
|
{"CLAUDECODE", "claude_code"},
|
|
23
23
|
{"CURSOR_AGENT", "cursor"},
|
|
24
24
|
{"GEMINI_CLI", "gemini_cli"},
|
|
25
|
+
{"OPENCODE", "opencode"},
|
|
26
|
+
{"QWEN_CODE", "qwen_code"},
|
|
27
|
+
{"CLINE_ACTIVE", "cline"},
|
|
28
|
+
{"COPILOT_CLI", "github_copilot_cli"},
|
|
29
|
+
{"COPILOT_AGENT_SESSION_ID", "github_copilot_cli"},
|
|
25
30
|
} {
|
|
26
31
|
value := strings.ToLower(strings.TrimSpace(getenv(marker.key)))
|
|
27
32
|
if value == "" || value == "0" || value == "false" {
|
|
@@ -14,9 +14,17 @@ func TestDetectHostSource(t *testing.T) {
|
|
|
14
14
|
{"claude", map[string]string{"CLAUDECODE": "1"}, "claude_code"},
|
|
15
15
|
{"cursor", map[string]string{"CURSOR_AGENT": "1"}, "cursor"},
|
|
16
16
|
{"gemini", map[string]string{"GEMINI_CLI": "1"}, "gemini_cli"},
|
|
17
|
+
{"opencode", map[string]string{"OPENCODE": "1"}, "opencode"},
|
|
18
|
+
{"qwen", map[string]string{"QWEN_CODE": "1"}, "qwen_code"},
|
|
19
|
+
{"cline", map[string]string{"CLINE_ACTIVE": " true "}, "cline"},
|
|
20
|
+
{"copilot cli", map[string]string{"COPILOT_CLI": "1"}, "github_copilot_cli"},
|
|
21
|
+
{"copilot session", map[string]string{"COPILOT_AGENT_SESSION_ID": "secret"}, "github_copilot_cli"},
|
|
22
|
+
{"copilot markers agree", map[string]string{"COPILOT_CLI": "1", "COPILOT_AGENT_SESSION_ID": "secret"}, "github_copilot_cli"},
|
|
17
23
|
{"ambiguous", map[string]string{"CODEX_THREAD_ID": "secret", "CURSOR_AGENT": "1"}, ""},
|
|
24
|
+
{"new markers conflict", map[string]string{"OPENCODE": "1", "CLINE_ACTIVE": "true"}, ""},
|
|
18
25
|
{"disabled markers", map[string]string{"CLAUDECODE": "0", "GEMINI_CLI": " false "}, ""},
|
|
19
|
-
{"
|
|
26
|
+
{"new markers disabled", map[string]string{"OPENCODE": "0", "QWEN_CODE": " FALSE ", "CLINE_ACTIVE": "", "COPILOT_CLI": "false", "COPILOT_AGENT_SESSION_ID": " "}, ""},
|
|
27
|
+
{"configuration is not runtime evidence", map[string]string{"CODEX_HOME": "/home/example", "ANTHROPIC_API_KEY": "secret", "TERM_PROGRAM": "vscode", "AGENT": "1", "OPENCODE_CONFIG": "/example/config.json", "QWEN_API_KEY": "secret", "COPILOT_HOME": "/example/copilot"}, ""},
|
|
20
28
|
} {
|
|
21
29
|
t.Run(tc.name, func(t *testing.T) {
|
|
22
30
|
if got := detectHostSource(func(key string) string { return tc.env[key] }); got != tc.want {
|
|
@@ -5,6 +5,7 @@ import (
|
|
|
5
5
|
|
|
6
6
|
"github.com/Pippit-dev/pippit-cli/internal/auth"
|
|
7
7
|
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
8
|
+
"github.com/Pippit-dev/pippit-cli/internal/space"
|
|
8
9
|
)
|
|
9
10
|
|
|
10
11
|
// AuthManager is the credential boundary shared by native CLI commands. It
|
|
@@ -20,9 +21,10 @@ type AuthManager interface {
|
|
|
20
21
|
|
|
21
22
|
// Runner carries runtime dependencies for command execution.
|
|
22
23
|
type Runner struct {
|
|
23
|
-
Config
|
|
24
|
-
Client
|
|
25
|
-
Auth
|
|
24
|
+
Config *config.Config
|
|
25
|
+
Client Client
|
|
26
|
+
Auth AuthManager
|
|
27
|
+
SpaceStore *space.Store
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
func NewRunner(cfg *config.Config, client Client) *Runner {
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
package common
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"net/url"
|
|
8
|
+
|
|
9
|
+
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
10
|
+
"github.com/Pippit-dev/pippit-cli/internal/space"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
type spaceContextKey struct{}
|
|
14
|
+
type spaceRequestKey struct{}
|
|
15
|
+
type spaceRequest struct {
|
|
16
|
+
operation string
|
|
17
|
+
refs space.References
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func WithSpace(ctx context.Context, session *space.Session) context.Context {
|
|
21
|
+
return context.WithValue(ctx, spaceContextKey{}, session)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func SpaceSession(ctx context.Context) *space.Session {
|
|
25
|
+
session, _ := ctx.Value(spaceContextKey{}).(*space.Session)
|
|
26
|
+
return session
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func SpaceTeamID(ctx context.Context) string { return SpaceSession(ctx).TeamID() }
|
|
30
|
+
|
|
31
|
+
// spaceOperation classifies successful responses for local records only.
|
|
32
|
+
// It does not decide whether a request carries the selected TeamID.
|
|
33
|
+
func spaceOperation(path string) string {
|
|
34
|
+
u, err := url.Parse(path)
|
|
35
|
+
if err != nil {
|
|
36
|
+
return ""
|
|
37
|
+
}
|
|
38
|
+
switch u.Path {
|
|
39
|
+
case config.SubmitRunPath, config.SubmitMarketingRunPath:
|
|
40
|
+
return "submit"
|
|
41
|
+
case config.GetThreadPath, config.QueryGenerateVideoResultPath:
|
|
42
|
+
return "query"
|
|
43
|
+
case config.ListThreadFilePath:
|
|
44
|
+
return "files"
|
|
45
|
+
case config.UploadFilePath:
|
|
46
|
+
return "upload"
|
|
47
|
+
}
|
|
48
|
+
return ""
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func SpaceReferences(body any) (space.References, error) {
|
|
52
|
+
var ref space.References
|
|
53
|
+
if body == nil {
|
|
54
|
+
return ref, nil
|
|
55
|
+
}
|
|
56
|
+
data, err := json.Marshal(body)
|
|
57
|
+
if err != nil {
|
|
58
|
+
return ref, err
|
|
59
|
+
}
|
|
60
|
+
if err := json.Unmarshal(data, &ref); err != nil {
|
|
61
|
+
return ref, fmt.Errorf("任务或素材 ID 格式无效: %w", err)
|
|
62
|
+
}
|
|
63
|
+
return ref, nil
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func (c *httpClient) prepareSpaceRequest(ctx context.Context, path string, body any) (context.Context, error) {
|
|
67
|
+
session := SpaceSession(ctx)
|
|
68
|
+
if session == nil {
|
|
69
|
+
return ctx, nil
|
|
70
|
+
}
|
|
71
|
+
if target, err := url.Parse(path); err == nil && target.IsAbs() {
|
|
72
|
+
trusted, err := c.isBaseURLOrigin(target)
|
|
73
|
+
if err != nil {
|
|
74
|
+
return ctx, err
|
|
75
|
+
}
|
|
76
|
+
if !trusted {
|
|
77
|
+
return ctx, nil
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
ref, err := SpaceReferences(body)
|
|
81
|
+
if err != nil {
|
|
82
|
+
return ctx, err
|
|
83
|
+
}
|
|
84
|
+
if _, err := session.Resolve(ref); err != nil {
|
|
85
|
+
return ctx, err
|
|
86
|
+
}
|
|
87
|
+
return context.WithValue(ctx, spaceRequestKey{}, spaceRequest{operation: spaceOperation(path), refs: ref}), nil
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func rememberSpaceResponse(ctx context.Context, data []byte) {
|
|
91
|
+
session := SpaceSession(ctx)
|
|
92
|
+
request, ok := ctx.Value(spaceRequestKey{}).(spaceRequest)
|
|
93
|
+
if session == nil || !ok || request.operation == "" {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
var response struct {
|
|
97
|
+
Ret json.RawMessage `json:"ret"`
|
|
98
|
+
Data *struct {
|
|
99
|
+
ThreadID string `json:"thread_id"`
|
|
100
|
+
RunID string `json:"run_id"`
|
|
101
|
+
PippitAssetID string `json:"pippit_asset_id"`
|
|
102
|
+
Run struct {
|
|
103
|
+
ThreadID string `json:"thread_id"`
|
|
104
|
+
RunID string `json:"run_id"`
|
|
105
|
+
} `json:"run"`
|
|
106
|
+
} `json:"data"`
|
|
107
|
+
}
|
|
108
|
+
if json.Unmarshal(data, &response) != nil || response.Data == nil || (string(response.Ret) != `"0"` && string(response.Ret) != "0") {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
ref := request.refs
|
|
112
|
+
switch request.operation {
|
|
113
|
+
case "submit":
|
|
114
|
+
if response.Data.Run.ThreadID == "" || response.Data.Run.RunID == "" {
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
if ref.ThreadID != "" && ref.ThreadID != response.Data.Run.ThreadID {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
ref.ThreadID, ref.RunID = response.Data.Run.ThreadID, response.Data.Run.RunID
|
|
121
|
+
case "query", "files":
|
|
122
|
+
if ref.ThreadID == "" {
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
if response.Data.ThreadID != "" && response.Data.ThreadID != ref.ThreadID {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
if response.Data.RunID != "" && response.Data.RunID != ref.RunID {
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
case "upload":
|
|
132
|
+
if response.Data.PippitAssetID == "" {
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
ref.AssetIDs = []string{response.Data.PippitAssetID}
|
|
136
|
+
default:
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
session.Remember(ref)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
func SpaceAccount(ctx context.Context, runner *Runner) (string, error) {
|
|
143
|
+
// Without an AuthManager, authentication belongs to the adapter. An unused
|
|
144
|
+
// environment AK must not stand in for the adapter's actual account.
|
|
145
|
+
if runner.Auth == nil {
|
|
146
|
+
return "", nil
|
|
147
|
+
}
|
|
148
|
+
if provider, ok := runner.Auth.(interface {
|
|
149
|
+
SpaceAccount(context.Context) (string, error)
|
|
150
|
+
}); ok {
|
|
151
|
+
return provider.SpaceAccount(ctx)
|
|
152
|
+
}
|
|
153
|
+
status, err := runner.Auth.Status(ctx)
|
|
154
|
+
if err != nil {
|
|
155
|
+
return "", err
|
|
156
|
+
}
|
|
157
|
+
if status != nil && status.UID != "" {
|
|
158
|
+
return "user:" + space.Digest(status.UID), nil
|
|
159
|
+
}
|
|
160
|
+
return "", nil
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// SpaceStoreForRunner returns nil when the current account cannot be identified.
|
|
164
|
+
// Callers can use an explicit space without persisting local records.
|
|
165
|
+
func SpaceStoreForRunner(ctx context.Context, runner *Runner) (*space.Store, error) {
|
|
166
|
+
account, err := SpaceAccount(ctx, runner)
|
|
167
|
+
if err != nil {
|
|
168
|
+
return nil, err
|
|
169
|
+
}
|
|
170
|
+
if account == "" {
|
|
171
|
+
return nil, nil
|
|
172
|
+
}
|
|
173
|
+
base := config.DefaultBaseURL
|
|
174
|
+
if runner.Config != nil && runner.Config.BaseURL != "" {
|
|
175
|
+
base = runner.Config.BaseURL
|
|
176
|
+
}
|
|
177
|
+
store, err := space.NewStore(base, account)
|
|
178
|
+
if err != nil {
|
|
179
|
+
return nil, err
|
|
180
|
+
}
|
|
181
|
+
if runner.SpaceStore != nil {
|
|
182
|
+
store.Dir = runner.SpaceStore.Dir
|
|
183
|
+
}
|
|
184
|
+
return store, nil
|
|
185
|
+
}
|