fix(canvas): 媒体提示词 - 统一 Agent 更新与画布生成读取生成合同
This commit is contained in:
1 parent
853c7d2ce7
commit
6d757b7001
20 files changed
+174
-48
No files matched your search
@@ -68,10 +68,11 @@ func cloudAgentCreativeAnchorForCanvas(repo *repository.Repository, userID strin
|
||||
continue
|
||||
}
|
||||
meta, _ := node["metadata"].(map[string]any)
|
||||
prompt, _ := cloudAgentMediaPrompt(meta)
|
||||
item := cloudAgentReferenceAnchor{
|
||||
NodeID: stringValue(node["id"]), Type: descriptor.Type,
|
||||
Title: truncateRunes(stringValue(node["title"]), 300),
|
||||
Prompt: truncateRunes(firstNonEmpty(stringValue(meta["prompt"]), stringValue(meta["composerContent"])), 1000),
|
||||
Prompt: truncateRunes(prompt, 1000),
|
||||
VisualIdentity: "unknown", RequiresVisualInspection: true,
|
||||
}
|
||||
if tags, ok := meta["assetTags"].([]any); ok {
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestCloudAgentCanvasApprovalPreviewDescribesUpdateTargetAndFields(t *testin
|
||||
if item.NodeTitle != "满月庆祝视频草稿" || item.NodeType != "video" || item.NodeTypeLabel != "视频" {
|
||||
t.Fatalf("preview lost target identity: %+v", item)
|
||||
}
|
||||
if item.ResultTitle != newTitle || strings.Join(item.Fields, "、") != "节点名称、下一版提示词" {
|
||||
if item.ResultTitle != newTitle || strings.Join(item.Fields, "、") != "节点名称、提示词" {
|
||||
t.Fatalf("preview lost changed fields: %+v", item)
|
||||
}
|
||||
raw, _ := json.Marshal(item)
|
||||
@@ -37,7 +37,8 @@ func TestCloudAgentCanvasApprovalPreviewDescribesUpdateTargetAndFields(t *testin
|
||||
t.Fatalf("preview exposed private content or internal id: %s", raw)
|
||||
}
|
||||
metadata := doc["nodes"].([]map[string]any)[0]["metadata"].(map[string]any)
|
||||
if metadata["content"] != "已提交结果" || metadata["prompt"] != "private prompt" || metadata["composerContent"] != newPrompt {
|
||||
generationSpec := metadata["generationSpec"].(map[string]any)
|
||||
if metadata["content"] != "已提交结果" || metadata["prompt"] != "private prompt" || generationSpec["prompt"] != newPrompt {
|
||||
t.Fatalf("preview mutated media result incorrectly: %+v", metadata)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,8 +216,13 @@ func TestCloudAgentCanvasOperationTraceSharesToolCallID(t *testing.T) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if cat == nil || stringValue(cat["title"]) != "叮当猫飞行参考" || stringValue(cat["metadata"].(map[string]any)["composerContent"]) != "下一版参考图提示词" {
|
||||
t.Fatalf("canvas_apply_ops did not persist the requested draft: %+v", cat)
|
||||
if cat == nil || stringValue(cat["title"]) != "叮当猫飞行参考" {
|
||||
t.Fatalf("canvas_apply_ops did not persist the requested prompt contract: %+v", cat)
|
||||
}
|
||||
metadata := cat["metadata"].(map[string]any)
|
||||
generationSpec := metadata["generationSpec"].(map[string]any)
|
||||
if stringValue(generationSpec["prompt"]) != "下一版参考图提示词" {
|
||||
t.Fatalf("canvas_apply_ops did not persist the requested prompt contract: %+v", cat)
|
||||
}
|
||||
|
||||
actions := creationMaps(trace["actions"])
|
||||
@@ -238,7 +243,7 @@ func TestCloudAgentCanvasOperationTraceSharesToolCallID(t *testing.T) {
|
||||
fieldValues = append(fieldValues, stringValue(field))
|
||||
}
|
||||
}
|
||||
if len(fieldValues) != 2 || fieldValues[0] != "节点名称" || fieldValues[1] != "下一版提示词" {
|
||||
if len(fieldValues) != 2 || fieldValues[0] != "节点名称" || fieldValues[1] != "提示词" {
|
||||
t.Fatalf("canvas operation trace lost updated fields: %+v", updated)
|
||||
}
|
||||
created := byActionAndNode["created:trace-video"]
|
||||
|
||||
@@ -440,9 +440,15 @@ func cloudAgentProjectNodeFields(node, meta map[string]any, descriptor capabilit
|
||||
}
|
||||
continue
|
||||
}
|
||||
value, ok := node[key]
|
||||
if !ok {
|
||||
value, ok = meta[key]
|
||||
var value any
|
||||
var ok bool
|
||||
if key == "prompt" && descriptor.GenerationMode != "" {
|
||||
value, ok = cloudAgentMediaPrompt(meta)
|
||||
} else {
|
||||
value, ok = node[key]
|
||||
if !ok {
|
||||
value, ok = meta[key]
|
||||
}
|
||||
}
|
||||
if !ok || (key == "content" && descriptor.GenerationMode != "") {
|
||||
continue
|
||||
@@ -457,6 +463,21 @@ func cloudAgentProjectNodeFields(node, meta map[string]any, descriptor capabilit
|
||||
return projected, nil
|
||||
}
|
||||
|
||||
func cloudAgentMediaPrompt(meta map[string]any) (string, bool) {
|
||||
if generationSpec, ok := meta["generationSpec"].(map[string]any); ok {
|
||||
if prompt, ok := generationSpec["prompt"].(string); ok {
|
||||
return prompt, true
|
||||
}
|
||||
}
|
||||
if prompt, ok := meta["composerContent"].(string); ok {
|
||||
return prompt, true
|
||||
}
|
||||
if prompt, ok := meta["prompt"].(string); ok {
|
||||
return prompt, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func cloudAgentProjectionValue(node, meta map[string]any, path string) (any, bool) {
|
||||
parts := strings.Split(path, ".")
|
||||
for _, root := range []map[string]any{node, meta} {
|
||||
|
||||
@@ -963,7 +963,8 @@ func TestCloudAgentCanvasUpdatesExistingVideoDraftThroughCapabilityContract(t *t
|
||||
}
|
||||
updated := updatedNodes["video-1789310237935-mmh3-baby-fullmoon"]
|
||||
metadata := updated["metadata"].(map[string]any)
|
||||
if updated["title"] != "满月庆祝视频草稿(舒缓呼吸感)" || metadata["composerContent"] != "下一版舒缓视频提示词" {
|
||||
generationSpec := metadata["generationSpec"].(map[string]any)
|
||||
if updated["title"] != "满月庆祝视频草稿(舒缓呼吸感)" || generationSpec["prompt"] != "下一版舒缓视频提示词" {
|
||||
t.Fatalf("video draft was not updated: %#v", updated)
|
||||
}
|
||||
if metadata["prompt"] != "原始已提交提示词" || metadata["status"] != "error" || metadata["referenceIssue"] != "参考资产尚未准备完成" {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package capability
|
||||
|
||||
import "infinite-canvas/backend/internal/canvas/contract"
|
||||
|
||||
const (
|
||||
maxAgentNodeTitleRunes = 240
|
||||
maxAgentNodeContentRunes = 16000
|
||||
@@ -136,10 +138,10 @@ func generatedMediaDescriptor(nodeType, version, label string, width, height flo
|
||||
Purpose: semantics.Purpose, GoodFor: semantics.GoodFor, NotIdealFor: semantics.NotIdealFor,
|
||||
Tradeoffs: semantics.Tradeoffs, Actions: semantics.Actions,
|
||||
InputKind: nodeType, GenerationMode: generationMode, Connection: connection, CanUpdate: true,
|
||||
SummaryFields: []string{"prompt", "composerContent", "assetTags", "referenceNodeIds"},
|
||||
DetailFields: []string{"prompt", "composerContent", "assetTags", "referenceNodeIds"},
|
||||
PatchFields: editableNodeFields("metadata.composerContent", "下一版提示词", "下次生成使用的提示词草稿;不覆盖已提交提示词或媒体结果"),
|
||||
CreateMetadata: generatedMetadata,
|
||||
SummaryFields: []string{"prompt", "assetTags", "referenceNodeIds"},
|
||||
DetailFields: []string{"prompt", "assetTags", "referenceNodeIds"},
|
||||
PatchFields: editableNodeFields("metadata.generationSpec.prompt", "提示词", "生成合同中的当前提示词;这是 Agent 唯一读写的媒体提示词"),
|
||||
CreateMetadata: func(prompt string) map[string]any { return generatedMetadata(generationMode, prompt) },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,6 +213,20 @@ func positionPatchFields() map[string]PatchField {
|
||||
}
|
||||
}
|
||||
|
||||
func generatedMetadata(prompt string) map[string]any {
|
||||
return map[string]any{"content": "", "prompt": prompt, "composerContent": prompt, "status": "idle"}
|
||||
func generatedMetadata(mode, prompt string) map[string]any {
|
||||
spec := contract.GenerationSpec{
|
||||
Version: contract.GenerationVersion,
|
||||
Mode: mode,
|
||||
Prompt: prompt,
|
||||
Options: contract.Options{},
|
||||
ReferenceBindings: []contract.ReferenceBinding{},
|
||||
TextInputMode: "append-sources",
|
||||
}
|
||||
metadata, err := spec.NodeMetadata()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
metadata["content"] = ""
|
||||
metadata["status"] = "idle"
|
||||
return metadata
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"infinite-canvas/backend/internal/canvas/contract"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -146,6 +148,23 @@ func (d Descriptor) ApplyPatch(node map[string]any, patch map[string]any) error
|
||||
}
|
||||
for key, value := range patch {
|
||||
field := d.PatchFields[key]
|
||||
if field.Path == "metadata.generationSpec.prompt" {
|
||||
metadata, ok := node["metadata"].(map[string]any)
|
||||
if !ok {
|
||||
metadata = map[string]any{}
|
||||
node["metadata"] = metadata
|
||||
}
|
||||
if _, ok := metadata["generationSpec"].(map[string]any); !ok {
|
||||
metadata["generationSpec"] = map[string]any{
|
||||
"version": float64(contract.GenerationVersion),
|
||||
"mode": d.GenerationMode,
|
||||
"prompt": "",
|
||||
"options": map[string]any{},
|
||||
"referenceBindings": []any{},
|
||||
"textInputMode": "append-sources",
|
||||
}
|
||||
}
|
||||
}
|
||||
parts := strings.Split(field.Path, ".")
|
||||
target := node
|
||||
for _, part := range parts[:len(parts)-1] {
|
||||
|
||||
@@ -241,8 +241,8 @@ func TestBuiltinMediaCapabilitiesUpdateDraftWithoutOverwritingGeneratedAssets(t
|
||||
if !ok || !descriptor.CanUpdate {
|
||||
t.Fatalf("media capability is not updateable: %#v", descriptor)
|
||||
}
|
||||
if descriptor.PatchFields["content"].Path != "metadata.composerContent" || descriptor.PatchFields["title"].Path != "title" {
|
||||
t.Fatalf("unsafe media update paths: %#v", descriptor.PatchFields)
|
||||
if descriptor.PatchFields["content"].Path != "metadata.generationSpec.prompt" || descriptor.PatchFields["title"].Path != "title" {
|
||||
t.Fatalf("media update must target the generation contract: %#v", descriptor.PatchFields)
|
||||
}
|
||||
|
||||
node := map[string]any{
|
||||
@@ -251,28 +251,70 @@ func TestBuiltinMediaCapabilitiesUpdateDraftWithoutOverwritingGeneratedAssets(t
|
||||
"content": "resource:generated-result",
|
||||
"prompt": "已提交提示词",
|
||||
"composerContent": "旧草稿",
|
||||
"status": "success",
|
||||
"generationSpec": map[string]any{
|
||||
"version": float64(1),
|
||||
"mode": nodeType,
|
||||
"prompt": "旧草稿",
|
||||
"options": map[string]any{},
|
||||
"referenceBindings": []any{},
|
||||
"textInputMode": "append-sources",
|
||||
},
|
||||
"status": "success",
|
||||
},
|
||||
}
|
||||
if err := descriptor.ApplyPatch(node, map[string]any{"title": "新标题", "content": "下一版提示词"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
metadata := node["metadata"].(map[string]any)
|
||||
if node["title"] != "新标题" || metadata["composerContent"] != "下一版提示词" {
|
||||
t.Fatalf("media draft was not updated: %#v", node)
|
||||
generationSpec := metadata["generationSpec"].(map[string]any)
|
||||
if node["title"] != "新标题" || generationSpec["prompt"] != "下一版提示词" || metadata["composerContent"] != "旧草稿" {
|
||||
t.Fatalf("media prompt contract was not updated: %#v", node)
|
||||
}
|
||||
if metadata["content"] != "resource:generated-result" || metadata["prompt"] != "已提交提示词" || metadata["status"] != "success" {
|
||||
if metadata["content"] != "resource:generated-result" || metadata["prompt"] != "已提交提示词" || metadata["status"] != "success" || generationSpec["prompt"] != "下一版提示词" {
|
||||
t.Fatalf("media result or submission snapshot was overwritten: %#v", metadata)
|
||||
}
|
||||
|
||||
created := descriptor.Metadata("初始草稿")
|
||||
if created["content"] != "" || created["prompt"] != "初始草稿" || created["composerContent"] != "初始草稿" || created["status"] != "idle" {
|
||||
createdSpec := created["generationSpec"].(map[string]any)
|
||||
if created["content"] != "" || created["prompt"] != "初始草稿" || created["composerContent"] != "初始草稿" || createdSpec["prompt"] != "初始草稿" || created["status"] != "idle" {
|
||||
t.Fatalf("media draft metadata is incomplete: %#v", created)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuiltinMediaCapabilitiesUpdateDraftInitializesEmptyGenerationContract(t *testing.T) {
|
||||
registry := BuiltinRegistry()
|
||||
for _, nodeType := range []string{"image", "video", "audio"} {
|
||||
t.Run(nodeType, func(t *testing.T) {
|
||||
descriptor, ok := registry.Resolve(nodeType)
|
||||
if !ok {
|
||||
t.Fatal("missing media descriptor")
|
||||
}
|
||||
node := map[string]any{
|
||||
"title": "旧媒体节点",
|
||||
"metadata": map[string]any{
|
||||
"content": "resource:already-generated",
|
||||
"prompt": "已提交快照",
|
||||
"composerContent": "旧兼容草稿",
|
||||
"status": "success",
|
||||
},
|
||||
}
|
||||
if err := descriptor.ApplyPatch(node, map[string]any{"content": "空合同后的新提示词"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
metadata := node["metadata"].(map[string]any)
|
||||
spec := metadata["generationSpec"].(map[string]any)
|
||||
if spec["mode"] != nodeType || spec["prompt"] != "空合同后的新提示词" {
|
||||
t.Fatalf("empty generation contract was not initialized: %#v", metadata)
|
||||
}
|
||||
if metadata["content"] != "resource:already-generated" || metadata["prompt"] != "已提交快照" || metadata["composerContent"] != "旧兼容草稿" || metadata["status"] != "success" {
|
||||
t.Fatalf("legacy media state was overwritten: %#v", metadata)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryResolveUnknownTypeFailsClosed(t *testing.T) {
|
||||
registry, err := NewRegistry([]Descriptor{testDescriptor()})
|
||||
if err != nil {
|
||||
|
||||
Reference in new issue
Block a user