@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.
- package/README.md +3 -8
- package/checksums.txt +6 -6
- package/dist/checksums.txt +6 -6
- package/dist/xyq-canvas-command-runtime.cjs +14 -14
- package/dist/xyq-canvas-command-runtime.cjs.LEGAL.txt +1 -27
- package/dist/xyq-canvas-command-runtime.cjs.sha256 +2 -2
- package/internal/canvas/image_binding.go +152 -0
- package/internal/canvas/image_binding_test.go +137 -0
- package/internal/canvas/media_identity.go +21 -12
- package/package.json +1 -1
- package/scripts/canvas-command-guides.js +13 -21
- package/scripts/canvas-command.js +11 -0
- package/skills/xyq-nest-skill/commands/canvas.md +6 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
|
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
|
|
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
|
|
128
|
+
summary: "3D V2 文档的能力边界、空间与动画单位。",
|
|
129
129
|
content: [
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
"
|
|
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: "
|
|
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(
|
|
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
|
-
|
|
|
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`。不再使用独立的创建、重连或删除连线命令;批量创建时让引用源节点排在目标节点之前。
|