@pippit-dev/cli 1.0.42 → 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 +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/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/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
|
+
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())
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
package common
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"io"
|
|
6
|
+
"net/http"
|
|
7
|
+
"net/http/httptest"
|
|
8
|
+
"os"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"testing"
|
|
11
|
+
"time"
|
|
12
|
+
|
|
13
|
+
"github.com/Pippit-dev/pippit-cli/internal/adapter"
|
|
14
|
+
"github.com/Pippit-dev/pippit-cli/internal/config"
|
|
15
|
+
"github.com/Pippit-dev/pippit-cli/internal/space"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
type spaceRewriteAdapter struct{ adapter.Default }
|
|
19
|
+
|
|
20
|
+
func (spaceRewriteAdapter) PrepareAPIRequest(_ context.Context, r *http.Request) (adapter.AuthDecision, error) {
|
|
21
|
+
r.URL.Path = "/rewritten" + r.URL.Path
|
|
22
|
+
return adapter.UseDefaultAuth, nil
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func TestSpaceHeadersAndTaskRecoveryThroughHTTP(t *testing.T) {
|
|
26
|
+
var headers []string
|
|
27
|
+
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
28
|
+
headers = append(headers, r.Header.Get("X-Team-Id"))
|
|
29
|
+
if r.Header.Get("Authorization") != "Bearer test-key" {
|
|
30
|
+
t.Error("missing authorization")
|
|
31
|
+
}
|
|
32
|
+
_, _ = io.Copy(io.Discard, r.Body)
|
|
33
|
+
switch r.URL.Path {
|
|
34
|
+
case config.UploadFilePath:
|
|
35
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"pippit_asset_id":"asset"}}`))
|
|
36
|
+
case config.SubmitRunPath:
|
|
37
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{"run":{"thread_id":"thread","run_id":"run"}}}`))
|
|
38
|
+
default:
|
|
39
|
+
_, _ = w.Write([]byte(`{"ret":"0","data":{}}`))
|
|
40
|
+
}
|
|
41
|
+
}))
|
|
42
|
+
defer api.Close()
|
|
43
|
+
store := &space.Store{Dir: t.TempDir(), Account: "account", Origin: api.URL}
|
|
44
|
+
if err := store.SaveDefault(space.Selection{TeamID: "12", Name: "A"}); err != nil {
|
|
45
|
+
t.Fatal(err)
|
|
46
|
+
}
|
|
47
|
+
account, identityChecks := store.Account, 0
|
|
48
|
+
ctx := WithSpace(context.Background(), &space.Session{Store: store, Identity: func(context.Context) (string, error) {
|
|
49
|
+
identityChecks++
|
|
50
|
+
return account, nil
|
|
51
|
+
}})
|
|
52
|
+
client := NewHTTPClient(api.URL, time.Second, NewAccessKeyAuthorizer("test-key"))
|
|
53
|
+
file := filepath.Join(t.TempDir(), "image.png")
|
|
54
|
+
if err := os.WriteFile(file, []byte("image"), 0600); err != nil {
|
|
55
|
+
t.Fatal(err)
|
|
56
|
+
}
|
|
57
|
+
var result map[string]any
|
|
58
|
+
if err := client.SendMultipartRequest(ctx, config.UploadFilePath, nil, MultipartFile{FieldName: "file", Path: file}, &result); err != nil {
|
|
59
|
+
t.Fatal(err)
|
|
60
|
+
}
|
|
61
|
+
if identityChecks != 1 {
|
|
62
|
+
t.Fatalf("identity checks per request = %d, want 1", identityChecks)
|
|
63
|
+
}
|
|
64
|
+
if err := store.SaveDefault(space.Selection{TeamID: "34", Name: "B"}); err != nil {
|
|
65
|
+
t.Fatal(err)
|
|
66
|
+
}
|
|
67
|
+
if err := client.SendRequest(ctx, config.SubmitRunPath, map[string]any{"asset_ids": []string{"asset"}}, &result); err != nil {
|
|
68
|
+
t.Fatal(err)
|
|
69
|
+
}
|
|
70
|
+
// A new command recovers the original task even after a saved-space switch.
|
|
71
|
+
queryCtx := WithSpace(context.Background(), &space.Session{Store: store})
|
|
72
|
+
if err := client.SendRequest(queryCtx, api.URL+config.GetThreadPath, space.References{ThreadID: "thread", RunID: "run"}, &result); err != nil {
|
|
73
|
+
t.Fatal(err)
|
|
74
|
+
}
|
|
75
|
+
if err := client.SendRequestWithHeaders(ctx, config.GetAvailableModelListPath, nil, map[string]string{"X-Team-Id": "999"}, &result); err != nil {
|
|
76
|
+
t.Fatal(err)
|
|
77
|
+
}
|
|
78
|
+
zero := "0"
|
|
79
|
+
personal := WithSpace(context.Background(), &space.Session{Store: store, Explicit: &zero})
|
|
80
|
+
if err := client.SendRequest(personal, config.GetCreditBalancePath, struct{}{}, &result); err != nil {
|
|
81
|
+
t.Fatal(err)
|
|
82
|
+
}
|
|
83
|
+
for i, want := range []string{"12", "12", "12", "12", ""} {
|
|
84
|
+
if headers[i] != want {
|
|
85
|
+
t.Errorf("request %d team = %q, want %q", i, headers[i], want)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
other := "34"
|
|
89
|
+
conflict := WithSpace(context.Background(), &space.Session{Store: store, Explicit: &other})
|
|
90
|
+
if err := client.SendRequest(conflict, config.GetThreadPath, space.References{ThreadID: "thread", RunID: "run"}, &result); err == nil {
|
|
91
|
+
t.Fatal("conflict sent")
|
|
92
|
+
}
|
|
93
|
+
if len(headers) != 5 {
|
|
94
|
+
t.Fatal("conflict reached API")
|
|
95
|
+
}
|
|
96
|
+
// A new API path still receives the selected space without a client allowlist.
|
|
97
|
+
unlisted := WithSpace(context.Background(), &space.Session{Store: store})
|
|
98
|
+
if err := client.SendRequest(unlisted, "/api/unlisted", struct{}{}, &result); err != nil {
|
|
99
|
+
t.Fatal(err)
|
|
100
|
+
}
|
|
101
|
+
if headers[len(headers)-1] != "34" {
|
|
102
|
+
t.Fatal("unlisted API did not receive the saved team")
|
|
103
|
+
}
|
|
104
|
+
foreign := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
105
|
+
if r.Header.Get("X-Team-Id") != "" || r.Header.Get("Authorization") != "" {
|
|
106
|
+
t.Error("foreign origin received sensitive headers")
|
|
107
|
+
}
|
|
108
|
+
_, _ = w.Write([]byte(`{}`))
|
|
109
|
+
}))
|
|
110
|
+
defer foreign.Close()
|
|
111
|
+
// A matching path on another origin must bypass both scope lookup and headers.
|
|
112
|
+
if err := client.SendRequestWithHeaders(conflict, foreign.URL+config.GetThreadPath, space.References{ThreadID: "thread", RunID: "run"}, map[string]string{"X-Team-Id": "999"}, &result); err != nil {
|
|
113
|
+
t.Fatal(err)
|
|
114
|
+
}
|
|
115
|
+
// Path rewriting by an adapter must preserve the original operation's scope.
|
|
116
|
+
rewritten := NewHTTPClientWithAdapter(api.URL, time.Second, NewAccessKeyAuthorizer("test-key"), spaceRewriteAdapter{})
|
|
117
|
+
if err := rewritten.SendRequest(ctx, config.GetCreditBalancePath, struct{}{}, &result); err != nil {
|
|
118
|
+
t.Fatal(err)
|
|
119
|
+
}
|
|
120
|
+
if headers[len(headers)-1] != "12" {
|
|
121
|
+
t.Fatal("adapter dropped space")
|
|
122
|
+
}
|
|
123
|
+
// Even with a frozen space, a changed account must stop before sending.
|
|
124
|
+
account = "other-account"
|
|
125
|
+
before := len(headers)
|
|
126
|
+
if err := client.SendRequest(ctx, config.GetCreditBalancePath, struct{}{}, &result); err == nil {
|
|
127
|
+
t.Fatal("account switch accepted")
|
|
128
|
+
}
|
|
129
|
+
if len(headers) != before {
|
|
130
|
+
t.Fatal("account switch reached API")
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -15,6 +15,7 @@ const AgentNameVideoPart = "pippit_video_part_agent"
|
|
|
15
15
|
|
|
16
16
|
// SubmitRunResult is the shared successful submit_run output.
|
|
17
17
|
type SubmitRunResult struct {
|
|
18
|
+
TeamID string `json:"team_id"`
|
|
18
19
|
ThreadID string `json:"thread_id"`
|
|
19
20
|
RunID string `json:"run_id"`
|
|
20
21
|
WebThreadLink string `json:"web_thread_link"`
|
|
@@ -86,6 +87,7 @@ func submitRun(ctx context.Context, command, path string, body any, runner *Runn
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
return &SubmitRunResult{
|
|
90
|
+
TeamID: SpaceTeamID(ctx),
|
|
89
91
|
ThreadID: resp.Data.Run.ThreadID,
|
|
90
92
|
RunID: resp.Data.Run.RunID,
|
|
91
93
|
WebThreadLink: resp.Data.WebThreadLink,
|
|
@@ -20,6 +20,7 @@ type UploadFileOptions struct {
|
|
|
20
20
|
|
|
21
21
|
// UploadFileResult is the shared JSON output for media and short-drama uploads.
|
|
22
22
|
type UploadFileResult struct {
|
|
23
|
+
TeamID string `json:"team_id"`
|
|
23
24
|
AssetID string `json:"asset_id"`
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -102,6 +103,7 @@ func UploadFile(ctx context.Context, opts UploadFileOptions, runner *Runner) (*U
|
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
return &UploadFileResult{
|
|
106
|
+
TeamID: SpaceTeamID(ctx),
|
|
105
107
|
AssetID: assetID,
|
|
106
108
|
}, nil
|
|
107
109
|
}
|
|
@@ -12,18 +12,21 @@ import (
|
|
|
12
12
|
)
|
|
13
13
|
|
|
14
14
|
const (
|
|
15
|
-
DefaultBaseURL
|
|
16
|
-
DefaultHTTPTimeout
|
|
17
|
-
DefaultAuthTTL
|
|
18
|
-
DefaultAuthStoreServiceName
|
|
19
|
-
SubmitRunPath
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
DefaultBaseURL = "https://xyq.jianying.com"
|
|
16
|
+
DefaultHTTPTimeout = 30 * time.Minute
|
|
17
|
+
DefaultAuthTTL = 30 * time.Second
|
|
18
|
+
DefaultAuthStoreServiceName = "pippit-cli"
|
|
19
|
+
SubmitRunPath = "/api/biz/v1/skill/submit_run"
|
|
20
|
+
SubmitMarketingRunPath = "/api/biz/v1/agent/submit_marketing_run"
|
|
21
|
+
QueryGenerateVideoResultPath = "/api/biz/v1/agent/query_generate_video_result"
|
|
22
|
+
GetCreditBalancePath = "/api/biz/v1/skill/get_credit_balance"
|
|
23
|
+
GetLiteTeamListPath = "/api/biz/v1/skill/get_lite_team_list"
|
|
24
|
+
GetAvailableModelListPath = "/api/biz/v1/skill/get_available_model_list"
|
|
25
|
+
GetThreadPath = "/api/biz/v1/skill/get_thread"
|
|
26
|
+
UploadFilePath = "/api/biz/v1/skill/upload_file"
|
|
27
|
+
ListThreadFilePath = "/api/biz/v1/skill/list_thread_file"
|
|
28
|
+
EnvXYQAccessKey = "XYQ_ACCESS_KEY"
|
|
29
|
+
FileName = "config.toml"
|
|
27
30
|
)
|
|
28
31
|
|
|
29
32
|
// Config holds runtime settings selected by the root command and passed down
|