@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.
Files changed (55) hide show
  1. package/README.md +42 -11
  2. package/checksums.txt +6 -6
  3. package/cmd/auth/auth.go +19 -5
  4. package/cmd/auth/auth_test.go +24 -1
  5. package/cmd/canvas/canvas.go +7 -7
  6. package/cmd/download_result.go +3 -2
  7. package/cmd/marketing.go +19 -3
  8. package/cmd/marketing_test.go +11 -5
  9. package/cmd/model.go +5 -4
  10. package/cmd/root.go +13 -5
  11. package/cmd/short_drama_test.go +2 -0
  12. package/cmd/source_test.go +27 -4
  13. package/cmd/space.go +199 -0
  14. package/cmd/space_test.go +469 -0
  15. package/cmd/upload_file_test.go +3 -1
  16. package/dist/checksums.txt +6 -6
  17. package/internal/adaptercli/commands_test.go +9 -5
  18. package/internal/auth/auth_test.go +21 -17
  19. package/internal/auth/loopback_origins_test.go +1 -1
  20. package/internal/auth/manager.go +1 -1
  21. package/internal/auth/space.go +26 -0
  22. package/internal/auth/space_test.go +42 -0
  23. package/internal/canvas/get.go +9 -2
  24. package/internal/commandnames/names.go +9 -4
  25. package/internal/common/client.go +23 -0
  26. package/internal/common/host_source.go +5 -0
  27. package/internal/common/host_source_test.go +9 -1
  28. package/internal/common/runner.go +5 -3
  29. package/internal/common/space.go +185 -0
  30. package/internal/common/space_test.go +132 -0
  31. package/internal/common/submit_run.go +2 -0
  32. package/internal/common/upload_file.go +2 -0
  33. package/internal/config/config.go +16 -12
  34. package/internal/generate_image/generate_image.go +3 -9
  35. package/internal/short_drama/submit_run.go +3 -9
  36. package/internal/space/session.go +153 -0
  37. package/internal/space/store.go +211 -0
  38. package/internal/space/store_test.go +246 -0
  39. package/package.json +1 -1
  40. package/scripts/canvas-command.js +37 -4
  41. package/scripts/install-wizard.js +1 -1
  42. package/scripts/run.js +2 -2
  43. package/scripts/telemetry.js +3 -1
  44. package/skills/short-drama/SKILL.md +12 -1
  45. package/skills/xyq-marketing-skill/SKILL.md +22 -3
  46. package/skills/xyq-marketing-skill/references/api.md +7 -1
  47. package/skills/xyq-marketing-skill/scripts/marketing.js +42 -18
  48. package/skills/xyq-nest-skill/SKILL.md +15 -4
  49. package/skills/xyq-nest-skill/commands/canvas-assets.md +1 -1
  50. package/skills/xyq-nest-skill/commands/canvas.md +5 -3
  51. package/skills/xyq-nest-skill/commands/get-credit-balance.md +2 -2
  52. package/skills/xyq-nest-skill/commands/model.md +1 -1
  53. package/skills/xyq-nest-skill/scripts/ensure-cli.js +8 -1
  54. package/skills/xyq-nest-skill/scripts/install.md +1 -1
  55. package/skills/xyq-nest-skill/workflows/canvas-edit.md +2 -2
@@ -0,0 +1,246 @@
1
+ package space
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "os"
7
+ "path/filepath"
8
+ "strings"
9
+ "testing"
10
+ )
11
+
12
+ func testStore(t *testing.T) *Store {
13
+ t.Helper()
14
+ return &Store{Dir: t.TempDir(), Account: "account-a", Origin: "https://example.test"}
15
+ }
16
+
17
+ func TestTaskScopeSurvivesDefaultSwitchAndRejectsConflicts(t *testing.T) {
18
+ s := testStore(t)
19
+ a := Selection{TeamID: "12", Name: "A"}
20
+ b := Selection{TeamID: "34", Name: "B"}
21
+ ref := References{ThreadID: "thread", RunID: "run", AssetIDs: []string{"asset"}}
22
+ if err := s.Remember(ref, a); err != nil {
23
+ t.Fatal(err)
24
+ }
25
+ if err := s.SaveDefault(b); err != nil {
26
+ t.Fatal(err)
27
+ }
28
+ got, err := (&Session{Store: s}).Resolve(ref)
29
+ if err != nil || got.TeamID != "12" {
30
+ t.Fatalf("old task = %+v, %v", got, err)
31
+ }
32
+ got, err = (&Session{Store: s}).Resolve(References{})
33
+ if err != nil || got.TeamID != "34" {
34
+ t.Fatalf("new task = %+v, %v", got, err)
35
+ }
36
+ if _, err := (&Session{Store: s, Explicit: &b.TeamID}).Resolve(ref); err == nil {
37
+ t.Fatal("explicit conflicting team accepted")
38
+ }
39
+ if _, err := (&Session{Store: s}).Resolve(References{AssetIDs: []string{"asset"}}); err == nil {
40
+ t.Fatal("cross-team asset accepted")
41
+ }
42
+ zero := "0"
43
+ got, err = (&Session{Store: s, Explicit: &zero}).Resolve(References{})
44
+ if err != nil || got.TeamID != "0" {
45
+ t.Fatalf("personal override = %+v, %v", got, err)
46
+ }
47
+ current, _ := s.Current()
48
+ if current.TeamID != "34" {
49
+ t.Fatal("one-shot override changed saved default")
50
+ }
51
+ if err := os.Remove(s.file("thread", "thread")); err != nil {
52
+ t.Fatal(err)
53
+ }
54
+ got, err = (&Session{Store: s}).Resolve(ref)
55
+ if err != nil || got.TeamID != "12" {
56
+ t.Fatalf("partial task record = %+v, %v", got, err)
57
+ }
58
+ }
59
+
60
+ func TestCorruptRecordRequiresExplicitRecovery(t *testing.T) {
61
+ s := testStore(t)
62
+ ref := References{ThreadID: "thread", RunID: "run"}
63
+ a := Selection{TeamID: "12", Name: "A"}
64
+ if err := s.Remember(ref, a); err != nil {
65
+ t.Fatal(err)
66
+ }
67
+ if err := os.WriteFile(s.file("run", "run"), []byte("broken"), 0600); err != nil {
68
+ t.Fatal(err)
69
+ }
70
+ if _, err := (&Session{Store: s}).Resolve(ref); err == nil {
71
+ t.Fatal("corrupt record treated as missing")
72
+ }
73
+ other := "34"
74
+ if _, err := (&Session{Store: s, Explicit: &other}).Resolve(ref); err == nil {
75
+ t.Fatal("corrupt run overrode valid thread")
76
+ }
77
+ if _, err := (&Session{Store: s, Explicit: &a.TeamID}).Resolve(ref); err != nil {
78
+ t.Fatal(err)
79
+ }
80
+ if err := s.Remember(ref, a); err != nil {
81
+ t.Fatal(err)
82
+ }
83
+ if _, err := (&Session{Store: s}).Resolve(ref); err != nil {
84
+ t.Fatal(err)
85
+ }
86
+ if err := os.WriteFile(s.file("default", ""), []byte("broken"), 0600); err != nil {
87
+ t.Fatal(err)
88
+ }
89
+ if _, err := (&Session{Store: s}).Resolve(References{}); err == nil {
90
+ t.Fatal("corrupt default fell back to personal")
91
+ }
92
+ zero := "0"
93
+ if _, err := (&Session{Store: s, Explicit: &zero}).Resolve(References{}); err != nil {
94
+ t.Fatal(err)
95
+ }
96
+ }
97
+
98
+ func TestSpaceIdentityAndInt64Boundary(t *testing.T) {
99
+ for _, raw := range []string{"", "-1", "1.5", " 12", "+12", "9223372036854775808"} {
100
+ if _, err := ParseTeamID(raw); err == nil {
101
+ t.Errorf("accepted %q", raw)
102
+ }
103
+ }
104
+ s := testStore(t)
105
+ for raw, want := range map[string]string{"0": "0", "000": "0", "00012": "12", "9223372036854775807": "9223372036854775807"} {
106
+ if got, err := ParseTeamID(raw); err != nil || got != want {
107
+ t.Errorf("%q = %q, %v", raw, got, err)
108
+ }
109
+ if err := s.SaveDefault(Selection{TeamID: raw}); err != nil {
110
+ t.Fatalf("save %q: %v", raw, err)
111
+ }
112
+ if got, err := s.Current(); err != nil || got.TeamID != want {
113
+ t.Errorf("read saved %q = %+v, %v; want team_id %q", raw, got, err, want)
114
+ }
115
+ }
116
+ a, err := NewStore("https://EXAMPLE.test:443/path", "account-a")
117
+ if err != nil {
118
+ t.Fatal(err)
119
+ }
120
+ b, _ := NewStore("https://example.test", "account-a")
121
+ c, _ := NewStore("https://example.test", "account-b")
122
+ d, _ := NewStore("https://other.test", "account-a")
123
+ if a.Dir != b.Dir || a.Dir == c.Dir || a.Dir == d.Dir {
124
+ t.Fatal("account/origin isolation failed")
125
+ }
126
+ }
127
+
128
+ func TestFixedSessionRequiresExplicitRecordRecovery(t *testing.T) {
129
+ for _, teamID := range []string{"0", "12"} {
130
+ for _, bindingOnly := range []bool{false, true} {
131
+ s := testStore(t)
132
+ selected := Selection{TeamID: teamID}
133
+ if err := s.SaveDefault(selected); err != nil {
134
+ t.Fatal(err)
135
+ }
136
+ session := &Session{Store: s}
137
+ if _, err := session.Resolve(References{}); err != nil {
138
+ t.Fatal(err)
139
+ }
140
+ if bindingOnly {
141
+ binding, err := DecodeBinding(session.Token())
142
+ if err != nil {
143
+ t.Fatal(err)
144
+ }
145
+ session = &Session{Store: s, Expected: binding}
146
+ }
147
+ if err := s.SaveDefault(Selection{TeamID: "34"}); err != nil {
148
+ t.Fatal(err)
149
+ }
150
+ ref := References{ThreadID: "thread", RunID: "run"}
151
+ if err := s.Remember(ref, selected); err != nil {
152
+ t.Fatal(err)
153
+ }
154
+ if err := os.WriteFile(s.file("run", "run"), []byte("broken"), 0600); err != nil {
155
+ t.Fatal(err)
156
+ }
157
+ if _, err := session.Resolve(ref); err == nil {
158
+ t.Fatalf("team %s, binding=%v: fixed space bypassed corrupt record", teamID, bindingOnly)
159
+ }
160
+ session.Explicit = &teamID
161
+ got, err := session.Resolve(ref)
162
+ if err != nil || got.TeamID != teamID {
163
+ t.Fatalf("explicit recovery = %+v, %v", got, err)
164
+ }
165
+ if err := s.Remember(References{ThreadID: "other"}, Selection{TeamID: "34"}); err != nil {
166
+ t.Fatal(err)
167
+ }
168
+ if _, err := session.Resolve(References{ThreadID: "other"}); err == nil {
169
+ t.Fatal("fixed space accepted a conflicting task")
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ func TestSessionWithoutStoreKeepsExplicitSpace(t *testing.T) {
176
+ for _, teamID := range []string{"0", "12", "34"} {
177
+ explicit := teamID
178
+ session := &Session{Explicit: &explicit}
179
+ selected, err := session.Resolve(References{})
180
+ if err != nil || selected.TeamID != teamID {
181
+ t.Fatalf("space = %+v, %v", selected, err)
182
+ }
183
+ explicit = "56"
184
+ selected, err = session.Resolve(References{ThreadID: "thread", RunID: "run"})
185
+ if err != nil || selected.TeamID != teamID {
186
+ t.Fatalf("command changed its frozen space: %+v, %v", selected, err)
187
+ }
188
+ session.Remember(References{ThreadID: "thread", RunID: "run"})
189
+ if session.Token() != "" {
190
+ t.Fatal("unknown account must not produce an identity binding")
191
+ }
192
+ }
193
+ session := &Session{Expected: &Binding{Selection: Personal(), Account: "account", Origin: "https://example.test"}}
194
+ if _, err := session.Resolve(References{}); err == nil {
195
+ t.Fatal("identity binding accepted without an identifiable account")
196
+ }
197
+ }
198
+
199
+ func TestCommandSessionFreezesScopeAndChecksIdentity(t *testing.T) {
200
+ s := testStore(t)
201
+ if err := s.SaveDefault(Selection{TeamID: "12", Name: "A"}); err != nil {
202
+ t.Fatal(err)
203
+ }
204
+ account := s.Account
205
+ var warning bytes.Buffer
206
+ session := &Session{Store: s, Stderr: &warning, Identity: func(context.Context) (string, error) { return account, nil }}
207
+ ctx := context.Background()
208
+ if _, err := session.Resolve(References{}); err != nil {
209
+ t.Fatal(err)
210
+ }
211
+ token := session.Token()
212
+ if err := s.SaveDefault(Selection{TeamID: "34", Name: "B"}); err != nil {
213
+ t.Fatal(err)
214
+ }
215
+ got, err := session.Resolve(References{})
216
+ if err != nil || got.TeamID != "12" {
217
+ t.Fatalf("session changed = %+v, %v", got, err)
218
+ }
219
+ binding, err := DecodeBinding(token)
220
+ if err != nil {
221
+ t.Fatal(err)
222
+ }
223
+ next := &Session{Store: s, Expected: binding}
224
+ got, err = next.Resolve(References{})
225
+ if err != nil || got.TeamID != "12" || next.Token() != token {
226
+ t.Fatalf("cross-process binding = %+v, %v", got, err)
227
+ }
228
+ account = "account-b"
229
+ if err := session.CheckIdentity(ctx); err == nil {
230
+ t.Fatal("account switch accepted")
231
+ }
232
+ next.Store = &Store{Dir: s.Dir, Account: account, Origin: s.Origin}
233
+ if _, err := next.Resolve(References{}); err == nil {
234
+ t.Fatal("binding allowed different account")
235
+ }
236
+ // A successful request stays successful even if the record directory fails.
237
+ blocked := filepath.Join(t.TempDir(), "file")
238
+ if err := os.WriteFile(blocked, []byte("file"), 0600); err != nil {
239
+ t.Fatal(err)
240
+ }
241
+ s.Dir = blocked
242
+ session.Remember(References{ThreadID: "created-thread", RunID: "created-run"})
243
+ if !strings.Contains(warning.String(), "created-run") || !strings.Contains(warning.String(), "不要重复提交") {
244
+ t.Fatal(warning.String())
245
+ }
246
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pippit-dev/cli",
3
- "version": "1.0.42",
3
+ "version": "1.0.44",
4
4
  "description": "Pippit CLI",
5
5
  "bin": {
6
6
  "pippit-tool-cli": "scripts/run.js"
@@ -48,13 +48,32 @@ const COMMAND_HELP = `用法:
48
48
  list 返回简短索引;describe 按层展示字段和可选操作,使用 --path 展开 schema_path。
49
49
  guide 按主题说明时间、引用和工作流;schema 显式导出完整 schema(可能很大)。
50
50
  这些发现入口无需登录;run 需要目标画布的访问权限。
51
+ run 使用默认空间,可通过 --team-id <ID> 覆盖;0 为个人空间。
51
52
  `;
52
53
 
54
+ function canvasSpaceArgs(args) {
55
+ const clean = [];
56
+ let teamID;
57
+ for (let i = 0; i < args.length; i++) {
58
+ if (args[i] === "--team-id" || args[i].startsWith("--team-id=")) {
59
+ const value = args[i] === "--team-id" ? args[++i] : args[i].slice("--team-id=".length);
60
+ if (teamID !== undefined || !/^\d+$/.test(value || "") || BigInt(value) > 9223372036854775807n) throw new Error("team-id 必须为唯一的 0 或正整数 ID");
61
+ teamID = BigInt(value).toString();
62
+ } else clean.push(args[i]);
63
+ }
64
+ return { args: clean, teamID };
65
+ }
66
+
53
67
  function isCanvasCommand(args) {
54
- return args[0] === "canvas" && args[1] === "command";
68
+ // Global team-id may appear before or after the command. Other flags retain
69
+ // their existing parser and are never forwarded into canvas mutation JSON.
70
+ let start = 0;
71
+ while (args[start] === "--team-id" || (args[start] || "").startsWith("--team-id=")) start += args[start] === "--team-id" ? 2 : 1;
72
+ return args[start] === "canvas" && args[start + 1] === "command";
55
73
  }
56
74
 
57
75
  function parseCanvasCommandArgs(args) {
76
+ args = canvasSpaceArgs(args).args;
58
77
  if (!isCanvasCommand(args)) throw new Error("仅支持 canvas command 命令");
59
78
  const values = args.slice(2);
60
79
  if (!values.length || values[0] === "--help" || values[0] === "-h") {
@@ -196,9 +215,12 @@ class NativeCanvasClient {
196
215
  this.invocation = invocation;
197
216
  this.cwd = options.cwd;
198
217
  this.stderr = options.stderr || process.stderr;
218
+ this.teamID = options.teamID;
199
219
  }
200
220
 
201
221
  async runJSON(args, input) {
222
+ if (args[0] === "canvas" && this.teamID !== undefined) args = [...args, "--team-id", this.teamID];
223
+ if (args[0] === "canvas" && this.spaceContext) args = [...args, "--space-context", this.spaceContext];
202
224
  const result = await spawnProcess(this.invocation, args, {
203
225
  cwd: this.cwd,
204
226
  input: input === undefined ? undefined : JSON.stringify(input),
@@ -207,6 +229,12 @@ class NativeCanvasClient {
207
229
  let output;
208
230
  try {
209
231
  output = JSON.parse(result.stdout);
232
+ // The first canvas get resolves the space in Go. Reuse its binding for
233
+ // every SDK transport call, even if another process changes the default.
234
+ if (args[0] === "canvas" && output.team_id !== undefined) {
235
+ this.teamID = output.team_id;
236
+ this.spaceContext = output.space_context;
237
+ }
210
238
  } catch (error) {
211
239
  throw new Error(`原生 CLI 未返回有效 JSON(${args.join(" ")}):${error.message}`);
212
240
  }
@@ -786,6 +814,7 @@ function executePublicCommand(route, definitions, input) {
786
814
  }
787
815
 
788
816
  async function runCanvasCommand(args, options = {}) {
817
+ const { teamID } = canvasSpaceArgs(args);
789
818
  const parsed = parseCanvasCommandArgs(args);
790
819
  const stdout = options.stdout || process.stdout;
791
820
  const stderr = options.stderr || process.stderr;
@@ -842,8 +871,11 @@ async function runCanvasCommand(args, options = {}) {
842
871
  const input = parseInputJSON(parsed, cwd);
843
872
  const route = routes.get(parsed.commandName);
844
873
  validatePublicCommandInput(route, input);
845
- const nativeClient = new NativeCanvasClient(options.nativeInvocation, { cwd, stderr });
874
+ const nativeClient = new NativeCanvasClient(options.nativeInvocation, { cwd, stderr, teamID });
846
875
  const authStatus = await nativeClient.ensureCanvasAccess(parsed.canvasId);
876
+ const credentialScope = authStatus.credential_scope && nativeClient.teamID && nativeClient.teamID !== "0"
877
+ ? `${authStatus.credential_scope}:team:${nativeClient.teamID}`
878
+ : authStatus.credential_scope;
847
879
  const requiresDurableCheckpoint = CHECKPOINT_COMMANDS.has(parsed.commandName) ||
848
880
  (parsed.commandName === "apply_mutations" && input.checkpointBefore === true);
849
881
  if (requiresDurableCheckpoint && !authStatus.credential_scope) {
@@ -853,7 +885,7 @@ async function runCanvasCommand(args, options = {}) {
853
885
  const checkpointStore = authStatus.credential_scope
854
886
  ? createFileCheckpointStore({
855
887
  canvasId: parsed.canvasId,
856
- credentialScope: authStatus.credential_scope,
888
+ credentialScope,
857
889
  stateDirectory: options.stateDirectory,
858
890
  })
859
891
  : undefined;
@@ -872,7 +904,7 @@ async function runCanvasCommand(args, options = {}) {
872
904
  const persistence = authStatus.credential_scope
873
905
  ? createFilePersistence({
874
906
  canvasId: parsed.canvasId,
875
- credentialScope: authStatus.credential_scope,
907
+ credentialScope,
876
908
  stateDirectory: options.stateDirectory,
877
909
  })
878
910
  : createMemoryPersistence();
@@ -963,6 +995,7 @@ module.exports = {
963
995
  createSchemaFactory,
964
996
  definitionToJSON,
965
997
  ensureNodeRuntimeGlobals,
998
+ canvasSpaceArgs,
966
999
  isCanvasCommand,
967
1000
  parseCanvasCommandArgs,
968
1001
  runCanvasCommand,
@@ -21,7 +21,7 @@ PIPPIT_CLI_INSTALL_PACKAGE can override the package/version to install.
21
21
 
22
22
  Options:
23
23
  --source HOST Real host identifier reported as host_platform; unknown hosts omit it
24
- Defaults to PIPPIT_CLI_SOURCE, then known host runtime markers; an explicit empty value omits host_platform
24
+ Missing or blank values default to PIPPIT_CLI_SOURCE, then known host runtime markers
25
25
  -h, --help Show this help without installing, updating, or sending telemetry
26
26
  `;
27
27
 
package/scripts/run.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const { execFileSync } = require('child_process');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
- const { isCanvasCommand, parseCanvasCommandArgs, runCanvasCommand } = require('./canvas-command');
6
+ const { isCanvasCommand, parseCanvasCommandArgs, runCanvasCommand, canvasSpaceArgs } = require('./canvas-command');
7
7
  const { maybeWarnNewVersion } = require('./version-check');
8
8
 
9
9
  const ext = process.platform === 'win32' ? '.exe' : '';
@@ -45,7 +45,7 @@ function main() {
45
45
  }
46
46
 
47
47
  const helpRequested = args.includes('--help') || args.includes('-h') || args[0] === 'help';
48
- if (isCanvasCommand(args) && (args[2] !== 'run' || helpRequested)) {
48
+ if (isCanvasCommand(args) && (canvasSpaceArgs(args).args[2] !== 'run' || helpRequested)) {
49
49
  // Discovery commands also need update notices. Keep help and invalid
50
50
  // commands free of network checks and cache writes.
51
51
  if (!helpRequested) {
@@ -21,12 +21,14 @@ function telemetryBaseURL() {
21
21
 
22
22
  // Keep the same attribution rules as native CLI common.DetectHostSource.
23
23
  function resolveHostSource(explicit) {
24
- if (explicit !== undefined) return explicit.trim();
24
+ if (explicit !== undefined && explicit.trim()) return explicit.trim();
25
25
  if ((process.env.PIPPIT_CLI_SOURCE || "").trim()) return process.env.PIPPIT_CLI_SOURCE.trim();
26
26
  const hosts = new Set();
27
27
  for (const [key, host] of [
28
28
  ["CODEX_THREAD_ID", "codex"], ["CODEX_SESSION_ID", "codex"],
29
29
  ["CLAUDECODE", "claude_code"], ["CURSOR_AGENT", "cursor"], ["GEMINI_CLI", "gemini_cli"],
30
+ ["OPENCODE", "opencode"], ["QWEN_CODE", "qwen_code"], ["CLINE_ACTIVE", "cline"],
31
+ ["COPILOT_CLI", "github_copilot_cli"], ["COPILOT_AGENT_SESSION_ID", "github_copilot_cli"],
30
32
  ]) {
31
33
  const value = (process.env[key] || "").trim().toLowerCase();
32
34
  if (value && value !== "0" && value !== "false") hosts.add(host);
@@ -16,6 +16,17 @@ metadata:
16
16
  2. 需要业务调用时先按 [授权](commands/auth.md) 执行 `status`,已有登录可复用。
17
17
  3. 按下表读取命中的命令说明。提交、续接和处理后端问题必须读 [创作与会话续接](workflows/creation.md);查询或取得产物必须读 [轮询与文件交付](workflows/poll-and-deliver.md)。
18
18
 
19
+ ## Space Selection
20
+
21
+ 业务命令支持个人和团队空间,复用同一个 CLI 登录凭据。执行前先检查 `pippit-tool-cli space --help` 和业务命令的 `--team-id`;旧版不支持时先更新 CLI,不能忽略团队选择后继续收费操作。
22
+
23
+ - `space list` 统一输出 JSON,列出个人和当前账号的团队;顶层 `current` 表示已保存的默认空间,列表项 `current: true` 标记当前选择,无法识别本地账号时顶层为 `null`。`team_id` 保留字符串,`0` 为个人。列表中的团队不代表有生成权限、有效订阅或可用积分,最终由服务端判断。
24
+ - 用户明确要求长期切换时,运行 `space use TEAM_ID`;`space use 0` 保存个人空间。`use` 必须指定 ID,成功后返回保存的空间,不等待交互输入。只为本次任务选择空间时使用 `--team-id TEAM_ID`,不修改用户默认值;不能按模糊名称或剩余积分自动选团队。
25
+ - 新创作未指定空间时,读取并沿用保存的默认空间,首次使用为个人。向用户说明本次空间与积分账户,不重复索要已有授权。一次工作流开始后保存 TeamID,分开的上传、提交、查询和余额命令均显式传相同 `--team-id`,不能中途重新读取默认值。
26
+ - 已有任务优先沿用提交结果中的 `team_id` 或 CLI 的任务记录;切换默认团队后可以直接按原 thread/run 查询,CLI 自动恢复原空间。不要把当前默认 TeamID 强加给旧任务。跨设备或旧任务没有记录时,使用已知原空间;未知且用户也未指定时先核实,不能轮流试团队。
27
+ - 保存上传结果的资产 ID 和 `team_id`,提交结果的 thread/run ID 和 `team_id`。跨空间资产或任务冲突时停止;无权限、会员失效或余额不足不回退个人或其它团队。提交成功但本地记录写入失败时,保留返回 ID,用原 `--team-id` 查询,不重新生成。
28
+ - CLI 向同源 API 统一透传当前团队的 TeamID;模型接口内部继续按个人身份查询;模型列表成功不代表团队有生成权限。Canvas 同样使用所选空间;操作已有画布时沿用该画布原空间,切换默认空间后仍显式传入原 `--team-id`。
29
+
19
30
  ## 意图路由
20
31
 
21
32
  | 用户意图 | CLI | 必读文档 |
@@ -44,7 +55,7 @@ metadata:
44
55
 
45
56
  ## 宿主来源统计
46
57
 
47
- 优先从宿主的系统身份、当前工具运行环境和明确的平台元数据确认真实宿主,并在每次提交主动填写 `--source`;不要仅因字段可选就跳过。缺少显式值时,CLI 依次尝试 `PIPPIT_CLI_SOURCE` 和已核实的宿主运行标记。只上报稳定的平台名,不上报标记中的会话 ID;来源冲突或仍无法确认则省略,不询问用户、不扫描登录凭据或安装目录。显式 `--source ""` 禁用该次自动归因。
58
+ 优先从宿主的系统身份、当前工具运行环境和明确的平台元数据确认真实宿主,并在每次提交主动填写 `--source`;不要仅因字段可选就跳过。未传 `--source`、显式空值或纯空白时,CLI 依次尝试 `PIPPIT_CLI_SOURCE` 和已核实的宿主运行标记。只上报稳定的平台名,不上报标记中的会话 ID;来源冲突或仍无法确认则省略,不询问用户、不扫描登录凭据或安装目录。
48
59
 
49
60
  仅 `short-drama +submit-run` 静默附加可选 `--source`,用于统计:豆包办公 `doubao_office`、WorkBuddy `workbuddy`、Codex `codex`;其它宿主使用可信环境中的真实稳定标识,无法确认则省略。不从创作文本猜来源,不附带版本、会话 ID、用户信息或 prompt,不向用户询问,不改写 `--message`。上传、查询、下载不附加此参数;旧 CLI 的 `--help` 不支持时省略,不为统计字段重提任务。
50
61
 
@@ -22,6 +22,25 @@ metadata:
22
22
 
23
23
  先按 [接口契约](references/api.md) 准备请求;需要完整执行示例时读 [商品图到营销视频](examples/product-video.md)。
24
24
 
25
+ ## Space Selection
26
+
27
+ 业务命令支持个人和团队空间,复用同一个 CLI 登录凭据。执行前先检查 `pippit-tool-cli space --help` 和业务命令的 `--team-id`;旧版不支持时先更新 CLI,不能忽略团队选择后继续收费操作。
28
+
29
+ - `space list` 统一输出 JSON,列出个人和当前账号的团队;顶层 `current` 表示已保存的默认空间,列表项 `current: true` 标记当前选择,无法识别本地账号时顶层为 `null`。`team_id` 保留字符串,`0` 为个人。列表中的团队不代表有生成权限、有效订阅或可用积分,最终由服务端判断。
30
+ - 用户明确要求长期切换时,运行 `space use TEAM_ID`;`space use 0` 保存个人空间。`use` 必须指定 ID,成功后返回保存的空间,不等待交互输入。只为本次任务选择空间时使用 `--team-id TEAM_ID`,不修改用户默认值;不能按模糊名称或剩余积分自动选团队。
31
+ - 新创作未指定空间时,读取并沿用保存的默认空间,首次使用为个人。向用户说明本次空间与积分账户,不重复索要已有授权。一次工作流开始后保存 TeamID,分开的上传、提交、查询和余额命令均显式传相同 `--team-id`,不能中途重新读取默认值。
32
+ - 已有任务优先沿用提交结果中的 `team_id` 或 CLI 的任务记录;切换默认团队后可以直接按原 thread/run 查询,CLI 自动恢复原空间。不要把当前默认 TeamID 强加给旧任务。跨设备或旧任务没有记录时,使用已知原空间;未知且用户也未指定时先核实,不能轮流试团队。
33
+ - 保存上传结果的资产 ID 和 `team_id`,提交结果的 thread/run ID 和 `team_id`。跨空间资产或任务冲突时停止;无权限、会员失效或余额不足不回退个人或其它团队。提交成功但本地记录写入失败时,保留返回 ID,用原 `--team-id` 查询,不重新生成。
34
+ - CLI 向同源 API 统一透传当前团队的 TeamID;模型接口内部继续按个人身份查询;模型列表成功不代表团队有生成权限。Canvas 同样使用所选空间;操作已有画布时沿用该画布原空间,切换默认空间后仍显式传入原 `--team-id`。
35
+
36
+ 营销脚本的 `upload / generate / query / balance` 都支持 `--team-id TEAM_ID`。预览也读取同一空间;`query --wait` 固定 TeamID,本地账号可识别时同时固定账号和 API origin,切换账号则停止。无本地账号记录时,显式传入原 `--team-id` 仍可轮询,由 API 按当前凭据鉴权;不能读取默认值代替原任务空间。不要手工构造内部 `space_context`;使用原任务查询命令恢复。不同脚本调用之间仍需由宿主保留任务原 TeamID。
37
+
38
+ ```bash
39
+ node scripts/marketing.js upload --file /path/to/product.png --team-id TEAM_ID
40
+ node scripts/marketing.js generate --request /path/to/request.json --execute --team-id TEAM_ID
41
+ node scripts/marketing.js query --thread-id THREAD_ID --run-id RUN_ID --wait --team-id TEAM_ID
42
+ ```
43
+
25
44
  ## Workflow
26
45
 
27
46
  1. 确认用户要生成的营销内容与商品素材。保留用户原始指令,不主动扩写或加入未经提供的卖点。仅查询、接入咨询或估算不提交生成;明确要求生成即可执行,不重复询问已给出的授权。必要的补充信息用宿主当前允许的 `request_user_input_async`、`request_user_input` 或 `ask_user_question` 询问,没有工具时用普通聊天。
@@ -42,9 +61,9 @@ node scripts/marketing.js balance
42
61
 
43
62
  ## Source Attribution
44
63
 
45
- 仅营销生成命令(`marketing generate` 或 `marketing.js generate`)静默附加可选 `--source`,由 CLI 映射为请求体 `platform`,不写入创作文本或请求文件。豆包办公填 `doubao_office`,WorkBuddy 填 `workbuddy`,Codex 填 `codex`;其它宿主使用可信环境中的真实稳定标识,未知则省略,不询问用户。不附带版本、会话 ID、用户信息或 prompt。首尾空白去除,未传或空值省略;首次生成和同一会话继续提交均适用。上传、查询、下载、余额不附加。旧 CLI 不支持时省略,不为来源统计重提任务。
64
+ 仅营销生成命令(`marketing generate` 或 `marketing.js generate`)静默附加可选 `--source`,由 CLI 映射为请求体 `platform`,不写入创作文本或请求文件。豆包办公填 `doubao_office`,WorkBuddy 填 `workbuddy`,Codex 填 `codex`;其它宿主使用可信环境中的真实稳定标识,未知则省略,不询问用户。不附带版本、会话 ID、用户信息或 prompt。首尾空白去除,未传或空值时先读取 `PIPPIT_CLI_SOURCE`、再尝试已核实的宿主运行标记,仍无法识别时省略;首次生成和同一会话继续提交均适用。上传、查询、下载、余额不附加。旧 CLI 不支持时省略,不为来源统计重提任务。
46
65
 
47
- 优先从宿主的系统身份、当前工具运行环境和明确的平台元数据确认真实宿主,并在每次提交主动填写 `--source`;不要仅因字段可选就跳过。缺少显式值时,CLI 依次尝试 `PIPPIT_CLI_SOURCE` 和已核实的宿主运行标记。只上报稳定的平台名,不上报标记中的会话 ID;来源冲突或仍无法确认则省略,不询问用户、不扫描登录凭据或安装目录。显式 `--source ""` 禁用该次自动归因。
66
+ 优先从宿主的系统身份、当前工具运行环境和明确的平台元数据确认真实宿主,并在每次提交主动填写 `--source`;不要仅因字段可选就跳过。未传 `--source`、显式空值或纯空白时,CLI 依次尝试 `PIPPIT_CLI_SOURCE` 和已核实的宿主运行标记。只上报稳定的平台名,不上报标记中的会话 ID;来源冲突或仍无法确认则省略,不询问用户、不扫描登录凭据或安装目录。
48
67
 
49
68
  ## Completion and Recovery
50
69
 
@@ -58,4 +77,4 @@ node scripts/marketing.js balance
58
77
 
59
78
  ## Scope
60
79
 
61
- 本 Skill 仅使用正式公开营销接口;不宣称支持团队空间切换。鉴权范围沿用当前 CLI 登录身份和服务端授权,公开请求没有 `TeamID` 字段,不自行加入团队字段或跨账号复用资产/任务 ID。沉浸式短片、火山引擎服务不在此脚本范围。
80
+ 本 Skill 使用 CLI 登录和服务端空间授权,通过 `--team-id` 选择个人或团队,CLI 将其写入 HTTP Header;不向营销请求 JSON 添加 `TeamID` / `team_id`,不跨账号复用资产或任务。沉浸式短片、火山引擎服务不在此脚本范围。
@@ -22,7 +22,7 @@ Base URL:`https://xyq.jianying.com`。全部使用 POST,`Accept: application
22
22
  | `general_agent_settings` | object | 必传,且必须包含非空 `video_model` |
23
23
  | `thread_id` | string | 可选,只在继续已有营销会话时使用 |
24
24
 
25
- 来源统计由生成命令的可选 `--source HOST` 提供,CLI 在提交及预览的请求体中映射为 `platform` 字符串;不传或去除首尾空白后为空时省略。请求文件仍只填写上表中的创作字段,不直接添加 `source` 或 `platform`。
25
+ 来源统计由生成命令的可选 `--source HOST` 提供,CLI 在提交及预览的请求体中映射为 `platform` 字符串;不传或去除首尾空白后为空时,依次尝试 `PIPPIT_CLI_SOURCE` 和已核实的宿主运行标记,仍未知则省略。请求文件仍只填写上表中的创作字段,不直接添加 `source` 或 `platform`。
26
26
 
27
27
  设置块必须传对象,且 `video_model` 必填。2026-09-22 实际调用及服务端 `validateMarketingGeneralAgentSettings` 均确认:`{}` 或仅有比例/时长会返回 `ret=2`、缺少 `video_model`。不再按文档中的默认策略说明发送空对象;预览和提交都先本地校验。用户未指定模型时先询问,已有明确选择授权时按授权选择,不隐式降级。
28
28
 
@@ -63,3 +63,9 @@ HTTP 成功还需检查 `ret` 为 `"0"` 或 `0`;失败保留 `errmsg` 和 `log
63
63
  查询接口透传 Run 状态;仅在 3 时提取媒体,4/5 时提取失败原因,其它状态返回空媒体数组。因此 9 的空 `video_urls` 不能解释为生成失败。状态 3 也不保证有视频;以真实媒体列表判断交付。未知数值保留原始响应并停止,不自动重提或无期限轮询。
64
64
 
65
65
  一条 thread 可以有多个 Run。问卷、达人图或工具执行确认会产生后续 Run,原 `run_id` 不会自动变成最新运行。确认后从网页或可用的 `pippit-tool-cli get-thread --thread-id THREAD_ID` 读取新的真实 run_id,再传给营销查询命令;不要猜 ID 或为查询创建新 Run。
66
+
67
+ ## Space Context
68
+
69
+ 原生 CLI 和 Skill 脚本均支持 `--team-id TEAM_ID`;`0` 为个人,正整数为团队。空间通过 API origin 上的 `X-Team-Id` Header 传递,保持本文件中的 JSON 请求字段不变。非 Canvas 业务默认使用 `space use` 保存的空间;旧任务按本地记录恢复原空间。团队列表为 `space list`,由 CLI 使用同一 AK 调用 `/api/biz/v1/skill/get_lite_team_list`,不直连 commerce Web 接口。
70
+
71
+ 营销响应增加 `team_id` 和供脚本固定轮询使用的 `space_context`;无可用本地账号记录时 `space_context` 为空,脚本通过显式 `--team-id` 固定轮询空间。二者只是本地路由信息,不能替代服务端鉴权。预览增加 `team_id`,不发送生成请求。权限或积分错误不自动切空间重试。