feat(canvas): 批量创作表 - 支持全局提示词、参考图编排与连线同步
This commit is contained in:
@@ -29,6 +29,7 @@ type cloudAgentBatchTableEditArgs struct {
|
||||
Patch map[string]any `json:"patch"`
|
||||
Operation string `json:"operation"`
|
||||
Concurrency int `json:"concurrency"`
|
||||
GlobalPrompt string `json:"globalPrompt"`
|
||||
}
|
||||
|
||||
type cloudAgentBatchTableMutationPlan struct {
|
||||
@@ -87,6 +88,12 @@ func batchTableNodeFromDocument(doc map[string]any, nodeID string) (map[string]a
|
||||
if !ok || !cloudAgentBatchConcurrencyAllowed(concurrency) {
|
||||
return nil, nil, nil, nil, BadAuthRequest("批量创作表包含无效的并发数")
|
||||
}
|
||||
if rawPrompt, exists := table["globalPrompt"]; exists {
|
||||
globalPrompt, ok := rawPrompt.(string)
|
||||
if !ok || utf8.RuneCountInString(globalPrompt) > maxCloudAgentBatchPromptRunes {
|
||||
return nil, nil, nil, nil, BadAuthRequest("批量创作表包含无效或过长的全局提示词")
|
||||
}
|
||||
}
|
||||
columns := creationMaps(table["referenceColumns"])
|
||||
if len(columns) < 1 || len(columns) > maxCloudAgentBatchReferences {
|
||||
return nil, nil, nil, nil, BadAuthRequest("批量创作表必须包含 1 到 6 个参考图列")
|
||||
@@ -182,7 +189,7 @@ func prepareCloudAgentBatchTableEdit(repo *repository.Repository, userID, canvas
|
||||
if err := validateCloudAgentID(args.NodeID, "批量创作表节点ID", 80); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allowedActions := map[string]bool{"append": true, "update": true, "remove": true, "set_operation": true, "set_concurrency": true, "add_reference_column": true}
|
||||
allowedActions := map[string]bool{"append": true, "update": true, "remove": true, "set_operation": true, "set_concurrency": true, "add_reference_column": true, "remove_reference_column": true, "set_global_prompt": true}
|
||||
if !allowedActions[args.Action] {
|
||||
return nil, BadAuthRequest("批量创作表操作无效")
|
||||
}
|
||||
@@ -302,6 +309,38 @@ func prepareCloudAgentBatchTableEdit(repo *repository.Repository, userID, canvas
|
||||
table["referenceColumns"] = mapsAsAny(columns)
|
||||
fields = []string{"参考图列"}
|
||||
summaryVerb = "新增参考图列到"
|
||||
case "remove_reference_column":
|
||||
if len(columns) <= 1 {
|
||||
return nil, BadAuthRequest("批量创作表至少保留1组参考图")
|
||||
}
|
||||
if args.RowID != "" || len(args.Patch) != 0 || args.Operation != "" || args.Concurrency != 0 {
|
||||
return nil, BadAuthRequest("减少参考图列不接受其他修改参数")
|
||||
}
|
||||
columns = columns[:len(columns)-1]
|
||||
for index, column := range columns {
|
||||
column["label"] = fmt.Sprintf("参考图 %d", index+1)
|
||||
}
|
||||
table["referenceColumns"] = mapsAsAny(columns)
|
||||
for _, row := range rows {
|
||||
row["inputNodeIds"] = cloudAgentBatchInputIDs(row["inputNodeIds"], len(columns))
|
||||
}
|
||||
table["rows"] = mapsAsAny(rows)
|
||||
fields = []string{"参考图列"}
|
||||
summaryVerb = "减少参考图列自"
|
||||
case "set_global_prompt":
|
||||
if utf8.RuneCountInString(args.GlobalPrompt) > maxCloudAgentBatchPromptRunes {
|
||||
return nil, BadAuthRequest("全局提示词必须是不超过20000字的文本")
|
||||
}
|
||||
if args.RowID != "" || len(args.Patch) != 0 || args.Operation != "" || args.Concurrency != 0 {
|
||||
return nil, BadAuthRequest("设置全局提示词不接受其他修改参数")
|
||||
}
|
||||
if strings.TrimSpace(args.GlobalPrompt) == "" {
|
||||
delete(table, "globalPrompt")
|
||||
} else {
|
||||
table["globalPrompt"] = args.GlobalPrompt
|
||||
}
|
||||
fields = []string{"全局提示词"}
|
||||
summaryVerb = "设置全局提示词到"
|
||||
}
|
||||
metadata["batchTable"] = table
|
||||
title := cloudAgentApprovalNodeTitle(node, "批量创作表")
|
||||
|
||||
@@ -48,7 +48,7 @@ func TestCloudAgentBatchTableCapabilityMatchesCanvasComponent(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatal("batch-table capability missing")
|
||||
}
|
||||
if descriptor.Label != "批量创作表" || descriptor.DefaultWidth != 900 || descriptor.DefaultHeight != 520 || descriptor.InputKind != "text" || descriptor.ProjectionKind != "batch_table" || descriptor.ProjectionField != "batchTable" {
|
||||
if descriptor.Label != "批量创作表" || descriptor.DefaultWidth != 1280 || descriptor.DefaultHeight != 560 || descriptor.InputKind != "text" || descriptor.ProjectionKind != "batch_table" || descriptor.ProjectionField != "batchTable" {
|
||||
t.Fatalf("batch-table capability differs from component contract: %+v", descriptor)
|
||||
}
|
||||
if err := descriptor.ValidateConnection("image"); err != nil {
|
||||
@@ -97,6 +97,20 @@ func TestCloudAgentBatchTableToolsAreScopedAndStructured(t *testing.T) {
|
||||
t.Fatalf("batch row patch schema missing %s", field)
|
||||
}
|
||||
}
|
||||
if properties["globalPrompt"] == nil {
|
||||
t.Fatal("batch table edit schema missing globalPrompt")
|
||||
}
|
||||
actionEnum, _ := properties["action"].(map[string]any)["enum"].([]string)
|
||||
foundGlobalPrompt := false
|
||||
for _, action := range actionEnum {
|
||||
if action == "set_global_prompt" {
|
||||
foundGlobalPrompt = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundGlobalPrompt {
|
||||
t.Fatalf("batch table edit schema missing set_global_prompt: %+v", actionEnum)
|
||||
}
|
||||
for _, forbidden := range []string{"outputNodeId", "taskId", "url", "storageKey", "metadata"} {
|
||||
if patch[forbidden] != nil {
|
||||
t.Fatalf("batch row patch schema exposes forbidden field %s", forbidden)
|
||||
@@ -187,6 +201,85 @@ func TestCloudAgentCanReadAndEditBatchTableRows(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudAgentCanSetBatchTableGlobalPrompt(t *testing.T) {
|
||||
s, canvas := cloudAgentBatchTableFixture(t)
|
||||
doc, err := creationDocument(canvas.PayloadJSON)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
policy, err := s.RuntimePolicy()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
appendCall := cloudAgentBatchTableCall(t, "canvas_edit_batch_table", "append-empty-prompt", map[string]any{
|
||||
"snapshotHash": cloudAgentCanvasHash(doc), "nodeId": "batch-1", "action": "append",
|
||||
"patch": map[string]any{"inputNodeIds": []string{"image-1", "image-2"}, "prompt": " "},
|
||||
})
|
||||
appendResult, err := applyCloudAgentBatchTableMutation(s.repo, "user", canvas.ID, appendCall, policy)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state := &cloudAgentRuntime{Request: CloudAgentRequest{CanvasID: canvas.ID}}
|
||||
readCall := cloudAgentBatchTableCall(t, "canvas_read_batch_table", "read-before-global", map[string]any{"nodeId": "batch-1", "offset": 0})
|
||||
before, err := cloudAgentReadTool(s.repo, "user", state, readCall)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
beforePreview := before.(map[string]any)["batchTable"].(map[string]any)["generationPreview"].(map[string]any)
|
||||
if beforePreview["readyRows"] != 0 || beforePreview["missingPromptRows"] != 1 {
|
||||
t.Fatalf("empty row prompt should not be ready before global prompt: %+v", beforePreview)
|
||||
}
|
||||
|
||||
setCall := cloudAgentBatchTableCall(t, "canvas_edit_batch_table", "set-global", map[string]any{
|
||||
"snapshotHash": appendResult.(map[string]any)["snapshotHash"], "nodeId": "batch-1", "action": "set_global_prompt",
|
||||
"globalPrompt": " 全局换装提示词 ",
|
||||
})
|
||||
setResult, err := applyCloudAgentBatchTableMutation(s.repo, "user", canvas.ID, setCall, policy)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
after, err := cloudAgentReadTool(s.repo, "user", state, readCall)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
table := after.(map[string]any)["batchTable"].(map[string]any)
|
||||
if table["globalPrompt"] != "全局换装提示词" && table["globalPrompt"] != " 全局换装提示词 " {
|
||||
t.Fatalf("global prompt was not projected: %+v", table)
|
||||
}
|
||||
preview := table["generationPreview"].(map[string]any)
|
||||
if preview["readyRows"] != 1 || preview["missingPromptRows"] != 0 {
|
||||
t.Fatalf("global prompt should make the row ready: %+v", preview)
|
||||
}
|
||||
|
||||
clearCall := cloudAgentBatchTableCall(t, "canvas_edit_batch_table", "clear-global", map[string]any{
|
||||
"snapshotHash": setResult.(map[string]any)["snapshotHash"], "nodeId": "batch-1", "action": "set_global_prompt",
|
||||
"globalPrompt": " ",
|
||||
})
|
||||
if _, err := applyCloudAgentBatchTableMutation(s.repo, "user", canvas.ID, clearCall, policy); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cleared, err := cloudAgentReadTool(s.repo, "user", state, readCall)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clearedTable := cleared.(map[string]any)["batchTable"].(map[string]any)
|
||||
if _, exists := clearedTable["globalPrompt"]; exists {
|
||||
t.Fatalf("empty global prompt should be cleared: %+v", clearedTable)
|
||||
}
|
||||
clearedPreview := clearedTable["generationPreview"].(map[string]any)
|
||||
if clearedPreview["readyRows"] != 0 {
|
||||
t.Fatalf("cleared global prompt should restore missing prompt: %+v", clearedPreview)
|
||||
}
|
||||
|
||||
reject := cloudAgentBatchTableCall(t, "canvas_edit_batch_table", "reject-global-row", map[string]any{
|
||||
"snapshotHash": cleared.(map[string]any)["snapshotHash"], "nodeId": "batch-1", "action": "set_global_prompt",
|
||||
"rowId": "row-should-not-be-accepted", "globalPrompt": "x",
|
||||
})
|
||||
if _, err := prepareCloudAgentBatchTableEdit(s.repo, "user", canvas.ID, reject); err == nil || !strings.Contains(err.Error(), "不接受其他修改参数") {
|
||||
t.Fatalf("set_global_prompt should reject row-level parameters: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudAgentBatchTableRejectsUnsafeAndStaleEdits(t *testing.T) {
|
||||
s, canvas := cloudAgentBatchTableFixture(t)
|
||||
doc, err := creationDocument(canvas.PayloadJSON)
|
||||
|
||||
@@ -192,14 +192,15 @@ func compileCloudAgentTools(req CloudAgentRequest, includeProfileTool bool) []ma
|
||||
"rowId": str("update/remove 使用 canvas_read_storyboard 返回的真实 rowId;append 留空"),
|
||||
"patch": cloudAgentStoryboardPatchSchema(),
|
||||
}, "snapshotHash", "nodeId", "action")
|
||||
add("canvas_edit_batch_table", "操作批量创作表组件:追加、修改或删除任务行,切换批量换装/创意生图,设置1/5/10并发,或新增参考图列。必须先用 canvas_read_batch_table 获取最新 snapshotHash 和真实 rowId。行 patch 仅允许 enabled、inputNodeIds、prompt;prompt 可使用读取结果中的 @参考图1、@参考图2 等 mentionToken 指代本行对应位置的图片。append 未传 inputNodeIds 时会继承上一行参考图;图片ID必须来自当前画布。不能写 outputNodeId、任务状态、URL、storageKey 或任意 metadata。本工具只编辑计划,不提交收费生成。", map[string]any{
|
||||
add("canvas_edit_batch_table", "操作批量创作表组件:追加、修改或删除任务行,切换批量换装/创意生图,设置1/5/10并发,新增或减少参考图列,或设置覆盖各任务的全局提示词。必须先用 canvas_read_batch_table 获取最新 snapshotHash 和真实 rowId。行 patch 仅允许 enabled、inputNodeIds、prompt;prompt 可使用读取结果中的 @参考图1、@参考图2 等 mentionToken 指代本行对应位置的图片。append 未传 inputNodeIds 时会继承上一行参考图;图片ID必须来自当前画布。不能写 outputNodeId、任务状态、URL、storageKey 或任意 metadata。本工具只编辑计划,不提交收费生成。", map[string]any{
|
||||
"snapshotHash": str("最近一次批量创作表读取返回的 snapshotHash"),
|
||||
"nodeId": str("真实批量创作表节点ID"),
|
||||
"action": map[string]any{"type": "string", "enum": []string{"append", "update", "remove", "set_operation", "set_concurrency", "add_reference_column"}},
|
||||
"action": map[string]any{"type": "string", "enum": []string{"append", "update", "remove", "set_operation", "set_concurrency", "add_reference_column", "remove_reference_column", "set_global_prompt"}},
|
||||
"rowId": str("update/remove 使用 canvas_read_batch_table 返回的真实 rowId;其他操作留空"),
|
||||
"patch": cloudAgentBatchTablePatchSchema(),
|
||||
"operation": map[string]any{"type": "string", "enum": []string{"try_on", "creative"}},
|
||||
"concurrency": map[string]any{"type": "integer", "enum": []int{1, 5, 10}},
|
||||
"globalPrompt": str("set_global_prompt 使用;非空时覆盖各任务提示词,空字符串清除全局提示词"),
|
||||
}, "snapshotHash", "nodeId", "action")
|
||||
opProperties := map[string]any{
|
||||
"type": map[string]any{"type": "string", "enum": []string{"add_node", "update_node", "connect_nodes"}},
|
||||
|
||||
@@ -51,12 +51,12 @@ func BuiltinRegistry() *Registry {
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "batch-table", Version: "1", Label: "批量创作表", DefaultWidth: 900, DefaultHeight: 520,
|
||||
Purpose: "面向电商批量换装和创意生图的结构化任务表;每行绑定最多六组画布图片、可用 @参考图1 等位置引用编写独立提示词,并追踪生成结果。",
|
||||
Type: "batch-table", Version: "1", Label: "批量创作表", DefaultWidth: 1280, DefaultHeight: 560,
|
||||
Purpose: "面向电商批量换装和创意生图的结构化任务表;每行绑定最多六组画布图片、可用 @参考图1 等位置引用编写独立提示词,也可设置全局提示词覆盖各行,并追踪生成结果。",
|
||||
GoodFor: []string{"商品与模特批量换装", "同一商品多场景创意图", "多组参考图组合生成", "批量结果追踪与失败重试"},
|
||||
NotIdealFor: []string{"通用数据库或库存管理", "单张图片快速试验", "多镜头叙事连续性"},
|
||||
Tradeoffs: []string{"参考图必须先作为图片节点进入画布", "批量提交会产生多项生成任务,执行前必须确认模型、数量和费用"},
|
||||
Actions: []string{"read_rows", "append_row", "update_row", "remove_row", "set_operation", "set_concurrency", "add_reference_column", "preview_batch_generation"},
|
||||
Actions: []string{"read_rows", "append_row", "update_row", "remove_row", "set_operation", "set_concurrency", "add_reference_column", "remove_reference_column", "set_global_prompt", "preview_batch_generation"},
|
||||
InputKind: "text",
|
||||
Connection: ConnectionPolicy{CanSource: true, CanTarget: true, AcceptedInputKinds: []string{"image"}},
|
||||
CanUpdate: true, SummaryFields: []string{"batchTable"}, DetailFields: []string{"batchTable"}, ProjectionKind: "batch_table", ProjectionField: "batchTable",
|
||||
|
||||
Reference in New Issue
Block a user