@pippit-dev/cli 1.0.49 → 1.0.50

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.
@@ -1,7 +1,7 @@
1
1
  XYQ Canvas Command Runtime - Bundled Software Notices
2
2
 
3
3
  Generated from the exact esbuild input graph. No build paths or timestamps are included.
4
- 12 Pippit first-party modules are bundled and are not listed by internal package name.
4
+ 14 Pippit first-party modules are bundled and are not listed by internal package name.
5
5
 
6
6
  ===== @babel/runtime@7.29.7 =====
7
7
  License metadata: MIT
@@ -546,32 +546,6 @@ maintained libraries used by this software which have their own
546
546
  licenses; we recommend you read them, as their terms may differ from the
547
547
  terms above.
548
548
 
549
- ===== three@0.184.0 =====
550
- License metadata: MIT
551
-
552
- --- LICENSE ---
553
- The MIT License
554
-
555
- Copyright © 2010-2026 three.js authors
556
-
557
- Permission is hereby granted, free of charge, to any person obtaining a copy
558
- of this software and associated documentation files (the "Software"), to deal
559
- in the Software without restriction, including without limitation the rights
560
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
561
- copies of the Software, and to permit persons to whom the Software is
562
- furnished to do so, subject to the following conditions:
563
-
564
- The above copyright notice and this permission notice shall be included in
565
- all copies or substantial portions of the Software.
566
-
567
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
568
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
569
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
570
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
571
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
572
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
573
- THE SOFTWARE.
574
-
575
549
  ===== tslib@2.6.2 =====
576
550
  License metadata: 0BSD
577
551
 
@@ -1,2 +1,2 @@
1
- 8e14b9a2c496491c2ee44da3ed950ac8301b67981dfaba3ab1e042cf8b937298 xyq-canvas-command-runtime.cjs
2
- 3b97217c41dfb3c298185c80b1e70c2b469f552450e53d00fc1dcf7098740ffb xyq-canvas-command-runtime.cjs.LEGAL.txt
1
+ 1c53a5f869ee7c148e9f993e318d4ea5b34c5e93e2ac0749dd3f47ecff946e4f xyq-canvas-command-runtime.cjs
2
+ c547c923eaf2e8c155f84aed934f42ad02855d669e8c0a890e38d049bc1ef8e7 xyq-canvas-command-runtime.cjs.LEGAL.txt
@@ -0,0 +1,152 @@
1
+ package canvas
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "fmt"
7
+ "reflect"
8
+ "strconv"
9
+ "strings"
10
+ )
11
+
12
+ var imageSubjectFields = []string{"appearanceinfo", "sourceinfos"}
13
+
14
+ func isImageSubjectField(field string) bool {
15
+ name := strings.ToLower(field)
16
+ return name == imageSubjectFields[0] || name == imageSubjectFields[1]
17
+ }
18
+
19
+ func nodeBindingContent(node map[string]json.RawMessage) map[string]json.RawMessage {
20
+ raw, _ := identityField(node, "content")
21
+ var content map[string]json.RawMessage
22
+ _ = json.Unmarshal(raw, &content)
23
+ return content
24
+ }
25
+
26
+ func decodeBindingValue(raw json.RawMessage) any {
27
+ decoder := json.NewDecoder(bytes.NewReader(raw))
28
+ decoder.UseNumber()
29
+ var value any
30
+ _ = decoder.Decode(&value)
31
+ return value
32
+ }
33
+
34
+ func hasSubjectBinding(value any) bool {
35
+ switch value := value.(type) {
36
+ case nil:
37
+ return false
38
+ case string:
39
+ return strings.TrimSpace(value) != ""
40
+ case []any:
41
+ return len(value) > 0
42
+ case map[string]any:
43
+ return len(value) > 0
44
+ default:
45
+ return true
46
+ }
47
+ }
48
+
49
+ func replacesImageSubjectBinding(patch PatchEntry) bool {
50
+ if patch.Path != "" || patch.Op != "replace" {
51
+ return false
52
+ }
53
+ var node map[string]json.RawMessage
54
+ if json.Unmarshal(patch.Value, &node) != nil {
55
+ return false
56
+ }
57
+ kind, _ := identityString(node, "type")
58
+ if kind != "biz/image" {
59
+ return false
60
+ }
61
+ for field, raw := range nodeBindingContent(node) {
62
+ if isImageSubjectField(field) && hasSubjectBinding(decodeBindingValue(raw)) {
63
+ return true
64
+ }
65
+ }
66
+ return false
67
+ }
68
+
69
+ // Legacy fields may be retained or cleared, but new image bindings use the
70
+ // image protocol. Do not infer a role/scene conversion from arbitrary content.
71
+ func validateImageSubjectBinding(nodeID string, node, previous map[string]json.RawMessage) error {
72
+ content := nodeBindingContent(node)
73
+ oldContent := nodeBindingContent(previous)
74
+ oldType, _ := identityString(previous, "type")
75
+ for field, raw := range content {
76
+ if !isImageSubjectField(field) {
77
+ continue
78
+ }
79
+ value := decodeBindingValue(raw)
80
+ if !hasSubjectBinding(value) {
81
+ continue
82
+ }
83
+ if oldType == "biz/image" && reflect.DeepEqual(value, decodeBindingValue(oldContent[field])) {
84
+ continue
85
+ }
86
+ return mediaIdentityError(nodeID, "/content/"+field,
87
+ "biz/image cannot bind role/scene content; use content.pippitAssetId for an image or a biz/role or biz/scene node")
88
+ }
89
+ return nil
90
+ }
91
+
92
+ // Project only the selected identity subtree, including nested subject fields.
93
+ // RFC 6902 array operations must not collapse a child patch into its parent.
94
+ func projectMediaIdentityValue(raw json.RawMessage, parts []string, patch PatchEntry) (json.RawMessage, error) {
95
+ if len(parts) == 0 {
96
+ return patch.Value, nil
97
+ }
98
+ value := bytes.TrimSpace(raw)
99
+ if len(value) == 0 {
100
+ return nil, fmt.Errorf("cannot validate a media identity without its content base")
101
+ }
102
+ key := parts[0]
103
+ if value[0] == '{' {
104
+ var object map[string]json.RawMessage
105
+ if err := json.Unmarshal(value, &object); err != nil {
106
+ return nil, err
107
+ }
108
+ if len(parts) == 1 && patch.Op == "remove" {
109
+ delete(object, key)
110
+ } else {
111
+ next, err := projectMediaIdentityValue(object[key], parts[1:], patch)
112
+ if err != nil {
113
+ return nil, err
114
+ }
115
+ object[key] = next
116
+ }
117
+ return json.Marshal(object)
118
+ }
119
+ if value[0] != '[' {
120
+ return nil, fmt.Errorf("cannot validate a nested identity patch on a scalar")
121
+ }
122
+ var array []json.RawMessage
123
+ if err := json.Unmarshal(value, &array); err != nil {
124
+ return nil, err
125
+ }
126
+ index, err := strconv.Atoi(key)
127
+ if key == "-" && len(parts) == 1 && patch.Op == "add" {
128
+ index, err = len(array), nil
129
+ }
130
+ if err != nil || index < 0 || index > len(array) || (len(key) > 1 && key[0] == '0') {
131
+ return nil, fmt.Errorf("invalid media identity array index %q", key)
132
+ }
133
+ if len(parts) == 1 && patch.Op == "add" {
134
+ array = append(array, nil)
135
+ copy(array[index+1:], array[index:])
136
+ array[index] = patch.Value
137
+ } else {
138
+ if index == len(array) {
139
+ return nil, fmt.Errorf("media identity array index %q does not exist", key)
140
+ }
141
+ if len(parts) == 1 && patch.Op == "remove" {
142
+ array = append(array[:index], array[index+1:]...)
143
+ } else {
144
+ next, err := projectMediaIdentityValue(array[index], parts[1:], patch)
145
+ if err != nil {
146
+ return nil, err
147
+ }
148
+ array[index] = next
149
+ }
150
+ }
151
+ return json.Marshal(array)
152
+ }
@@ -0,0 +1,137 @@
1
+ package canvas
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "fmt"
7
+ "strings"
8
+ "testing"
9
+
10
+ "github.com/Pippit-dev/pippit-cli/internal/common"
11
+ )
12
+
13
+ func imageBindingFixture(node string, patches []PatchEntry) (ApplyOptions, *common.Runner, *int) {
14
+ opts, _, writes := applyIdentityFixture(patches, imageMaterial())
15
+ client := &fakeClient{send: func(_ context.Context, path string, body, out any) error {
16
+ switch path {
17
+ case QueryPath:
18
+ request := body.(queryRequest)
19
+ var assets []json.RawMessage
20
+ for _, id := range request.PippitAssetIDs {
21
+ if id == testNodeID {
22
+ content, _ := json.Marshal(node)
23
+ assets = append(assets, json.RawMessage(fmt.Sprintf(`{"PippitAssetID":"%s","TextInfo":{"content":%s},"version":5}`, id, content)))
24
+ } else if id == testPippitID {
25
+ assets = append(assets, json.RawMessage(imageMaterial()))
26
+ }
27
+ }
28
+ payload, _ := json.Marshal(map[string]any{"ret": "0", "data": map[string]any{"Assets": assets}})
29
+ return decodeInto(out, string(payload))
30
+ case ApplyPath:
31
+ *writes++
32
+ return decodeInto(out, fmt.Sprintf(`{"ret":"0","data":{"results":[{"transaction_id":"tx","status":"ack","asset_versions":{"%s":6}}]}}`, testNodeID))
33
+ default:
34
+ return fmt.Errorf("unexpected request %s", path)
35
+ }
36
+ }}
37
+ return opts, runnerWithClient(client), writes
38
+ }
39
+
40
+ func bindingPatch(op, path, value string) PatchEntry {
41
+ return PatchEntry{AssetID: testNodeID, Op: op, Path: path, Value: json.RawMessage(value)}
42
+ }
43
+
44
+ func TestApplyMediaIdentityRejectsRoleFieldsOnImageBeforeWrite(t *testing.T) {
45
+ blank := `{"type":"biz/image","content":{"caption":"draft","generation":{}}}`
46
+ for _, test := range []struct {
47
+ name, node string
48
+ patches []PatchEntry
49
+ }{
50
+ {"original-upload", blank, []PatchEntry{
51
+ bindingPatch("add", "/content/appearanceInfo", `{"pippitAssetId":"image-material"}`),
52
+ bindingPatch("add", "/content/generation", `{"source":"uploaded"}`),
53
+ bindingPatch("add", "/content/sourceInfos", `{"image-material":{"source":"uploaded"}}`),
54
+ }},
55
+ {"source-map", blank, []PatchEntry{bindingPatch("add", "/content/sourceInfos", `{"image-material":{}}`)}},
56
+ {"source-array", blank, []PatchEntry{bindingPatch("add", "/content/sourceInfos", `[{"pippitAssetId":"image-material"}]`)}},
57
+ {"root-create", "", []PatchEntry{bindingPatch("add", "", `{"type":"biz/image","content":{"appearanceInfo":{"pippitAssetId":"image-material"}}}`)}},
58
+ {"root-replace", blank, []PatchEntry{bindingPatch("replace", "", `{"type":"biz/image","content":{"sourceInfos":{"image-material":{}}}}`)}},
59
+ {"content-replace", blank, []PatchEntry{bindingPatch("replace", "/content", `{"appearanceInfo":{"candidateId":"candidate"}}`)}},
60
+ {"nested-appearance", `{"type":"biz/image","content":{"appearanceInfo":{}}}`, []PatchEntry{bindingPatch("add", "/content/appearanceInfo/pippitAssetId", `"image-material"`)}},
61
+ {"nested-source-map", `{"type":"biz/image","content":{"sourceInfos":{}}}`, []PatchEntry{bindingPatch("add", "/content/sourceInfos/material", `{"pippitAssetId":"image-material"}`)}},
62
+ {"nested-source-array", `{"type":"biz/image","content":{"sourceInfos":[]}}`, []PatchEntry{bindingPatch("add", "/content/sourceInfos/-", `{"pippitAssetId":"image-material"}`)}},
63
+ {"changed-legacy-field", `{"type":"biz/image","content":{"appearanceInfo":{"pippitAssetId":"old"}}}`, []PatchEntry{bindingPatch("replace", "/content/appearanceInfo/pippitAssetId", `"new"`)}},
64
+ {"remove-one-legacy-source", `{"type":"biz/image","content":{"sourceInfos":{"first":{},"second":{}}}}`, []PatchEntry{bindingPatch("remove", "/content/sourceInfos/first", "")}},
65
+ {"unchanged-direct-identities", string(imageNode(testMediaID)), []PatchEntry{bindingPatch("add", "/content/sourceInfos", `{"material":{}}`)}},
66
+ {"type-change", `{"type":"biz/role","content":{"appearanceInfo":{"pippitAssetId":"material"}}}`, []PatchEntry{bindingPatch("replace", "/type", `"biz/image"`)}},
67
+ {"case-alias", blank, []PatchEntry{bindingPatch("add", "/content/AppearanceInfo", `{"pippitAssetId":"material"}`)}},
68
+ } {
69
+ t.Run(test.name, func(t *testing.T) {
70
+ opts, runner, writes := imageBindingFixture(test.node, test.patches)
71
+ _, err := Apply(context.Background(), opts, runner)
72
+ if err == nil || !strings.Contains(err.Error(), "INVALID_MEDIA_IDENTITY") || !strings.Contains(err.Error(), "/content/") || !strings.Contains(err.Error(), "content.pippitAssetId") {
73
+ t.Fatalf("expected corrective image binding error, got %v", err)
74
+ }
75
+ if *writes != 0 {
76
+ t.Fatalf("invalid image binding was sent %d times", *writes)
77
+ }
78
+ })
79
+ }
80
+ }
81
+
82
+ func TestApplyMediaIdentityPreservesImageDraftAndLegacyEdits(t *testing.T) {
83
+ legacy := `{"type":"biz/image","content":{"caption":"old","appearanceInfo":{"pippitAssetId":"material","candidateId":"candidate"},"sourceInfos":{"material":{}}}}`
84
+ for _, test := range []struct {
85
+ name, node string
86
+ patches []PatchEntry
87
+ }{
88
+ {"blank-create", "", []PatchEntry{bindingPatch("add", "", `{"type":"biz/image","content":{"caption":"draft"}}`)}},
89
+ {"legacy-caption", legacy, []PatchEntry{bindingPatch("replace", "/content/caption", `"edited"`)}},
90
+ {"unchanged-content", legacy, []PatchEntry{bindingPatch("replace", "/content", `{"sourceInfos":{"material":{}},"appearanceInfo":{"candidateId":"candidate","pippitAssetId":"material"},"caption":"edited"}`)}},
91
+ {"unchanged-root", legacy, []PatchEntry{bindingPatch("replace", "", `{"type":"biz/image","content":{"sourceInfos":{"material":{}},"appearanceInfo":{"candidateId":"candidate","pippitAssetId":"material"},"caption":"edited"}}`)}},
92
+ {"unchanged-nested-field", legacy, []PatchEntry{bindingPatch("replace", "/content/appearanceInfo/pippitAssetId", `"material"`)}},
93
+ {"remove-field", legacy, []PatchEntry{bindingPatch("remove", "/content/appearanceInfo", "")}},
94
+ {"clear-object", legacy, []PatchEntry{bindingPatch("replace", "/content/appearanceInfo", `{}`)}},
95
+ {"clear-array", legacy, []PatchEntry{bindingPatch("replace", "/content/sourceInfos", `[]`)}},
96
+ {"clear-null", legacy, []PatchEntry{bindingPatch("replace", "/content/sourceInfos", `null`)}},
97
+ {"clear-blank", legacy, []PatchEntry{bindingPatch("replace", "/content/appearanceInfo", `" "`)}},
98
+ {"clear-last-object-key", `{"type":"biz/image","content":{"sourceInfos":{"a/b~c":{}}}}`, []PatchEntry{bindingPatch("remove", "/content/sourceInfos/a~1b~0c", "")}},
99
+ {"clear-last-array-item", `{"type":"biz/image","content":{"sourceInfos":[{"pippitAssetId":"material"}]}}`, []PatchEntry{bindingPatch("remove", "/content/sourceInfos/0", "")}},
100
+ {"final-role-type", `{"type":"biz/image","content":{}}`, []PatchEntry{
101
+ bindingPatch("add", "/content/appearanceInfo", `{"pippitAssetId":"material"}`),
102
+ bindingPatch("replace", "/type", `"biz/role"`),
103
+ }},
104
+ {"final-empty-binding", `{"type":"biz/image","content":{}}`, []PatchEntry{
105
+ bindingPatch("add", "/content/appearanceInfo", `{"pippitAssetId":"material"}`),
106
+ bindingPatch("remove", "/content/appearanceInfo", ""),
107
+ }},
108
+ } {
109
+ t.Run(test.name, func(t *testing.T) {
110
+ opts, runner, writes := imageBindingFixture(test.node, test.patches)
111
+ if _, err := Apply(context.Background(), opts, runner); err != nil {
112
+ t.Fatal(err)
113
+ }
114
+ if *writes != 1 {
115
+ t.Fatalf("writes = %d", *writes)
116
+ }
117
+ })
118
+ }
119
+ }
120
+
121
+ func TestApplyMediaIdentityLeavesOtherNodeBindingsUnchanged(t *testing.T) {
122
+ for _, kind := range []string{"role", "scene", "audio", "video"} {
123
+ t.Run(kind, func(t *testing.T) {
124
+ node := fmt.Sprintf(`{"type":"biz/%s","content":{"appearanceInfo":{},"sourceInfos":[]}}`, kind)
125
+ opts, runner, writes := imageBindingFixture(node, []PatchEntry{
126
+ bindingPatch("add", "/content/appearanceInfo/pippitAssetId", `"material"`),
127
+ bindingPatch("add", "/content/sourceInfos/-", `{"pippitAssetId":"material"}`),
128
+ })
129
+ if _, err := Apply(context.Background(), opts, runner); err != nil {
130
+ t.Fatal(err)
131
+ }
132
+ if *writes != 1 {
133
+ t.Fatalf("writes = %d", *writes)
134
+ }
135
+ })
136
+ }
137
+ }
@@ -4,6 +4,7 @@ import (
4
4
  "context"
5
5
  "encoding/json"
6
6
  "fmt"
7
+ "maps"
7
8
  "strings"
8
9
 
9
10
  "github.com/Pippit-dev/pippit-cli/internal/common"
@@ -91,10 +92,17 @@ func cloudMediaID(raw json.RawMessage, kind string) (string, error) {
91
92
  }
92
93
 
93
94
  func touchesMediaIdentity(path string) bool {
94
- switch strings.ToLower(path) {
95
+ normalized := strings.ToLower(path)
96
+ switch normalized {
95
97
  case "", "/type", "/content", "/content/assetid", "/content/pippitassetid":
96
98
  return true
97
99
  }
100
+ for _, field := range imageSubjectFields {
101
+ prefix := "/content/" + field
102
+ if normalized == prefix || strings.HasPrefix(normalized, prefix+"/") {
103
+ return true
104
+ }
105
+ }
98
106
  return false
99
107
  }
100
108
 
@@ -143,6 +151,9 @@ func projectIdentityPatch(node map[string]json.RawMessage, patch PatchEntry) (ma
143
151
  return nil, fmt.Errorf("cannot validate a patch without its node base")
144
152
  }
145
153
  parts := strings.Split(strings.TrimPrefix(patch.Path, "/"), "/")
154
+ for index, part := range parts {
155
+ parts[index] = strings.NewReplacer("~1", "/", "~0", "~").Replace(part)
156
+ }
146
157
  key := parts[0]
147
158
  if len(parts) == 1 {
148
159
  if patch.Op == "remove" {
@@ -152,16 +163,7 @@ func projectIdentityPatch(node map[string]json.RawMessage, patch PatchEntry) (ma
152
163
  }
153
164
  return node, nil
154
165
  }
155
- var content map[string]json.RawMessage
156
- if err := json.Unmarshal(node[key], &content); err != nil || content == nil {
157
- return nil, fmt.Errorf("cannot validate a media identity without its content base")
158
- }
159
- if patch.Op == "remove" {
160
- delete(content, parts[1])
161
- } else {
162
- content[parts[1]] = patch.Value
163
- }
164
- value, err := json.Marshal(content)
166
+ value, err := projectMediaIdentityValue(node[key], parts[1:], patch)
165
167
  if err != nil {
166
168
  return nil, err
167
169
  }
@@ -210,12 +212,13 @@ func validateApplyMediaIdentities(ctx context.Context, request ApplyRequest, run
210
212
  if touchesMediaIdentity(patch.Path) && !affected[patch.AssetID] {
211
213
  affected[patch.AssetID] = true
212
214
  ordered = append(ordered, patch.AssetID)
213
- if patch.Path != "" {
215
+ if patch.Path != "" || replacesImageSubjectBinding(patch) {
214
216
  toQuery = append(toQuery, patch.AssetID)
215
217
  }
216
218
  }
217
219
  }
218
220
  nodes := make(map[string]map[string]json.RawMessage)
221
+ previousNodes := make(map[string]map[string]json.RawMessage)
219
222
  type identity struct{ nodeID, kind, pippitID, mediaID string }
220
223
  previous := make(map[string]identity)
221
224
  if len(toQuery) > 0 {
@@ -230,6 +233,7 @@ func validateApplyMediaIdentities(ctx context.Context, request ApplyRequest, run
230
233
  return mediaIdentityError(id, "/content", err.Error())
231
234
  }
232
235
  nodes[id] = node
236
+ previousNodes[id] = maps.Clone(node)
233
237
  kind, pippitID, mediaID, readErr := readNodeIdentity(node)
234
238
  if readErr == nil {
235
239
  previous[id] = identity{id, kind, pippitID, mediaID}
@@ -254,6 +258,11 @@ func validateApplyMediaIdentities(ctx context.Context, request ApplyRequest, run
254
258
  if err != nil {
255
259
  return mediaIdentityError(nodeID, "/content", err.Error())
256
260
  }
261
+ if kind == "image" {
262
+ if err := validateImageSubjectBinding(nodeID, nodes[nodeID], previousNodes[nodeID]); err != nil {
263
+ return err
264
+ }
265
+ }
257
266
  if kind == "" || (pippitID == "" && mediaID == "") {
258
267
  continue
259
268
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pippit-dev/cli",
3
- "version": "1.0.49",
3
+ "version": "1.0.50",
4
4
  "description": "Pippit CLI",
5
5
  "bin": {
6
6
  "pippit-tool-cli": "scripts/run.js"
@@ -76,13 +76,13 @@ const GUIDES = [
76
76
  "多轨 TimelineDraft 的时间字段使用整数微秒:1 秒 = 1000000 µs。输出 width/height 使用像素。草稿 duration 由片段推导;frameRate 当前只读,没有设置帧率 command。expectedRevision 是草稿版本号,不是时间。",
77
77
  "3D 关键帧与动作片段 startFrame/durationFrames 使用帧,timeline.fps 定义帧时长;move_keyframes.deltaFrames 是整数帧偏移。动作片段 trimStart/trimEnd 使用源动画秒数,可带小数,不是帧。",
78
78
  "通用 generate-video --duration 使用整数秒,由该视频入口处理;它不会在本地把故事板 duration-ms 标签自动转换成该参数。其模型/时长能力不能替代故事板当前模型约束。",
79
- "ID 从所属查询取得:Timeline 的轨道/片段/来源 ID 由 xyq.timeline.query 返回;3D 对象/动画 ID 由 xyq.scene3d.query 返回。外层 nodeId 始终从当前 Canvas 节点查询取得。",
79
+ "ID 从所属查询取得:Timeline 的轨道/片段/来源 ID 由 xyq.timeline.query 返回;当前 CLI 未提供 3D V2 内部对象编辑入口。外层 nodeId 始终从当前 Canvas 节点查询取得。",
80
80
  ].join("\n\n"),
81
81
  next_commands: [
82
82
  "pippit-tool-cli canvas command guide storyboard",
83
83
  "pippit-tool-cli canvas command guide timeline",
84
84
  "pippit-tool-cli canvas command guide scene3d",
85
- "pippit-tool-cli canvas command describe xyq.scene3d.apply --operation move_keyframes",
85
+ "pippit-tool-cli canvas command list",
86
86
  ],
87
87
  examples: [
88
88
  {
@@ -125,31 +125,21 @@ const GUIDES = [
125
125
  },
126
126
  {
127
127
  name: "scene3d",
128
- summary: "3D 导演文档、对象 ID、空间与动画单位及编辑边界。",
128
+ summary: "3D V2 文档的能力边界、空间与动画单位。",
129
129
  content: [
130
- "3D 导演台由外层 Canvas 节点和独立导演文档组成。create_biz_node 使用 nodeKind: scene3d;先 list 确认当前安装版本支持的命令,再 describe --node-kind 查看工厂参数。",
131
- "xyq.scene3d.query 的 nodeId 是外层 Canvas 节点 ID;返回的对象、摄像机、关键帧等 ID 属于导演文档,不能与 Canvas 节点 ID 混用。先查询文档与可动画属性通道,再引用已有 ID。创建新对象时选文档内未占用的 ID。",
132
- "xyq.scene3d.apply.operations 是 {command,args} 数组,command 使用现有 Scene3D SDK 命令名。describe --operation 查看一种操作及其 args,schema 可导出完整输入。dryRun:true 预演编辑,不改变原文档。",
133
- "transform.position 使用米,rotation 为 XYZ Euler 角度,scale 为无单位 XYZ 倍率;camera FOV 使用角度。几何体 theta/phi/arc 等参数使用弧度,以对应 kind 的 schema 为准。",
134
- "关键帧与 startFrame/durationFrames 使用帧,帧时长取决于 timeline.fps;动作片段 trimStart/trimEnd 使用源动画秒数,可为小数。不要把微秒、毫秒或旋转弧度填入帧或角度字段。",
135
- "这套命令编辑导演文档;截图、渲染导出、上传与生成需要相应运行环境和现有入口。资源/动作引用应来自已查询到的真实资源,不用临时 URL 或虚构 ID 代替。",
130
+ "当前 SDK 主干的 3D 导演文档已迁移到 V2,旧 xyq.scene3d.apply/query 入口已移除。本版本不提供内部对象编辑命令,也不能通过通用资产补丁绕过该边界。内部编辑请使用 Web 3D 导演台。",
131
+ "外层节点仍可通过 create_biz_node 的 nodeKind: scene3d 创建;参数以 describe --node-kind 和当前 schema 为准。",
132
+ "transform.position 使用米,rotation 为 XYZ Euler 角度,scale 为无单位 XYZ 倍率;camera FOV 使用角度。几何体 theta/phi/arc 等参数使用弧度。",
133
+ "关键帧使用帧,帧时长取决于 timeline.fps;动作片段 trimStart/trimEnd 使用源动画秒数。此处仅说明单位,不代表 CLI 已提供编辑能力。",
136
134
  ].join("\n\n"),
137
135
  next_commands: [
136
+ "pippit-tool-cli canvas command list",
138
137
  "pippit-tool-cli canvas command describe create_biz_node --node-kind scene3d",
139
- 'pippit-tool-cli canvas command run xyq.scene3d.query --canvas-id CANVAS_ID --input \'{"nodeId":"DIRECTOR_NODE_ID"}\'',
140
- "pippit-tool-cli canvas command describe xyq.scene3d.apply --operation create_node",
141
- "pippit-tool-cli canvas command schema xyq.scene3d.apply",
142
138
  ],
143
139
  examples: [
144
140
  {
145
- description: "填入真实外层 nodeId,并确保 camera-new 在导演文档内未占用,再预演创建摄像机。",
146
- command:
147
- "pippit-tool-cli canvas command run xyq.scene3d.apply --canvas-id CANVAS_ID --file ./scene3d-input.json",
148
- input: {
149
- nodeId: "DIRECTOR_NODE_ID",
150
- operations: [{ command: "create_node", args: { kind: "camera", id: "camera-new", name: "Close-up" } }],
151
- dryRun: true,
152
- },
141
+ description: "只查询外层 3D 节点的创建 schema,不提交文档编辑。",
142
+ command: "pippit-tool-cli canvas command describe create_biz_node --node-kind scene3d",
153
143
  },
154
144
  ],
155
145
  related: ["time", "timeline"],
@@ -163,7 +153,9 @@ function listGuides() {
163
153
  function getGuide(name) {
164
154
  const guide = GUIDES.find((item) => item.name === name);
165
155
  if (!guide) {
166
- throw new Error(`未知的画布指南:${String(name)}。运行 pippit-tool-cli canvas command guide 查看主题。`);
156
+ throw new Error(
157
+ `未知的画布指南:${String(name)}。运行 pippit-tool-cli canvas command guide 查看主题。`,
158
+ );
167
159
  }
168
160
  return JSON.parse(JSON.stringify(guide));
169
161
  }
@@ -674,6 +674,17 @@ function createDefinitions(sdk, runtime, allocateNodeId, queryMediaAsset) {
674
674
  }
675
675
 
676
676
  function ensureNodeRuntimeGlobals() {
677
+ if (typeof globalThis.DOMException !== "function") {
678
+ // Node 16 exposes its native DOMException through AbortSignal.reason,
679
+ // while newer Node versions also expose the constructor as a global.
680
+ const controller = new AbortController();
681
+ controller.abort();
682
+ Object.defineProperty(globalThis, "DOMException", {
683
+ configurable: true,
684
+ value: controller.signal.reason.constructor,
685
+ writable: true,
686
+ });
687
+ }
677
688
  if (typeof globalThis.structuredClone !== "function") {
678
689
  const { deserialize, serialize } = require("v8");
679
690
  Object.defineProperty(globalThis, "structuredClone", {
@@ -27,7 +27,8 @@
27
27
  | 读取画布和资产 | `get_snapshot`、`get_asset`、`get_permissions` | 取得真实节点/资产 ID,不用名称猜 ID |
28
28
  | 创建业务节点 | `create_biz_node` | 支持类型和初始字段用 `--node-kind` 查看;不要手拼业务节点结构 |
29
29
  | 节点位置、大小、样式、布局、分组 | `move_nodes`、`resize_node`、`set_node_style`、`align_nodes`、`arrange_nodes`、`group_nodes` 等 | 先查询目标和归属;删除与重排仅限用户请求范围 |
30
- | 连线和画布配置 | `create_edge`、`reconnect_edge`、`delete_edges`、`set_title`、`set_cover` 等 | 连线两端来自当前画布查询 |
30
+ | 节点引用 | `create_biz_node`、`update_node_data` | 创建时用 `references: ["source-node-id"]`;更新时在 `generation.references` 声明 `{ "sourceNodeId": "source-node-id" }`,SDK 自动同步连线 |
31
+ | 画布配置 | `set_title`、`set_cover` 等 | 参数以当前命令 schema 为准 |
31
32
  | 角色与场景资料 | `xyq.role.updateDescription/updateAppearance/updateSourceInfos`、`xyq.scene.updateDescription` | 用完整命令名分别 describe,遵守具体字段 |
32
33
  | 图片/视频节点提示词和已有引用 | `xyq.generation.update_prompt` | 先读 `guide prompt-references`;保留其他生成参数,不触发生成 |
33
34
  | 3D 导演台对象、摄像机、关键帧、动作 | `xyq.scene3d.query` / `xyq.scene3d.apply` | 先读 `guide scene3d` 和 `guide time` |
@@ -51,3 +52,7 @@ node "CANVAS_ENTRY" canvas command guide
51
52
  - 目前没有公开的故事板镜头增删排序、脚本保存、指定镜头生成领域命令;`guide storyboard` 是说明,不是执行能力。底层补丁不能替代这些业务流程。
52
53
  - 3D 和多轨命令编辑文档,不提供截图、渲染或最终视频导出。多轨时间为整数微秒,3D 动画时间区分帧和源动画秒;先查字段 schema。
53
54
  - 编辑后按 [画布查询、编辑与验证](../workflows/canvas-edit.md) 回读确认;无需使用媒体异步查询来判断画布写入成功。只有实际取得图片/视频文件时才进入媒体交付标准。
55
+
56
+ 图片和视频空节点会按 `initialData.generation.ratio` 预留尺寸,再参与排版。先创建节点、设置生成比例和引用,再布局,避免产物回填后挤占间距。已有媒体优先使用真实尺寸。
57
+
58
+ 替换 `generation` 时必须同时携带完整的 `references` 列表;空数组表示清空引用。仅修改提示词可用 `xyq.generation.update_prompt`。不再使用独立的创建、重连或删除连线命令;批量创建时让引用源节点排在目标节点之前。