diff --git a/shortcuts/im/convert_lib/content_convert.go b/shortcuts/im/convert_lib/content_convert.go index 1ede9b79e..1292b7d33 100644 --- a/shortcuts/im/convert_lib/content_convert.go +++ b/shortcuts/im/convert_lib/content_convert.go @@ -131,7 +131,7 @@ func FormatMessageItem(m map[string]interface{}, runtime *common.RuntimeContext, if len(senderNames) > 0 { nameCache = senderNames[0] } - return formatMessageItem(m, runtime, nameCache, nil) + return formatMessageItem(m, runtime, nameCache, nil, false) } // FormatMessageItemWithMergePrefetch is like FormatMessageItem but threads a @@ -141,10 +141,20 @@ func FormatMessageItem(m map[string]interface{}, runtime *common.RuntimeContext, // items should pre-fetch once and call this variant in the loop to avoid the // N × ~1s serial-merge_forward stall in the original code path. func FormatMessageItemWithMergePrefetch(m map[string]interface{}, runtime *common.RuntimeContext, nameCache map[string]string, mergePrefetch map[string][]map[string]interface{}) map[string]interface{} { - return formatMessageItem(m, runtime, nameCache, mergePrefetch) + return formatMessageItem(m, runtime, nameCache, mergePrefetch, false) } -func formatMessageItem(m map[string]interface{}, runtime *common.RuntimeContext, nameCache map[string]string, mergePrefetch map[string][]map[string]interface{}) map[string]interface{} { +// FormatMessageItemWithMergePrefetchOpts is FormatMessageItemWithMergePrefetch +// with an explicit extractResources gate. When extractResources is true and +// the message carries downloadable resources, a "resources" block (ref list +// without local_path/size_bytes) is attached for the download enrichment stage +// to fill. The other entry points are thin extractResources=false wrappers, so +// default output is unchanged. +func FormatMessageItemWithMergePrefetchOpts(m map[string]interface{}, runtime *common.RuntimeContext, nameCache map[string]string, mergePrefetch map[string][]map[string]interface{}, extractResources bool) map[string]interface{} { + return formatMessageItem(m, runtime, nameCache, mergePrefetch, extractResources) +} + +func formatMessageItem(m map[string]interface{}, runtime *common.RuntimeContext, nameCache map[string]string, mergePrefetch map[string][]map[string]interface{}, extractResources bool) map[string]interface{} { msgType, _ := m["msg_type"].(string) messageId, _ := m["message_id"].(string) mentions, _ := m["mentions"].([]interface{}) @@ -152,8 +162,9 @@ func formatMessageItem(m map[string]interface{}, runtime *common.RuntimeContext, updated, _ := m["updated"].(bool) content := "" + rawContent := "" if body, ok := m["body"].(map[string]interface{}); ok { - rawContent, _ := body["content"].(string) + rawContent, _ = body["content"].(string) content = ConvertBodyContent(msgType, &ConvertContext{ RawContent: rawContent, MentionMap: BuildMentionKeyMap(mentions), @@ -232,6 +243,20 @@ func formatMessageItem(m map[string]interface{}, runtime *common.RuntimeContext, msg["mentions"] = simplified } + if extractResources { + if refs := ExtractResourceRefs(msgType, rawContent, messageId, mergePrefetch); len(refs) > 0 { + resources := make([]map[string]interface{}, 0, len(refs)) + for _, r := range refs { + resources = append(resources, map[string]interface{}{ + "message_id": r.MessageID, + "key": r.Key, + "type": r.Type, + }) + } + msg["resources"] = resources + } + } + return msg } diff --git a/shortcuts/im/convert_lib/content_media_misc_test.go b/shortcuts/im/convert_lib/content_media_misc_test.go index 3d0dbdaea..b6b2be25d 100644 --- a/shortcuts/im/convert_lib/content_media_misc_test.go +++ b/shortcuts/im/convert_lib/content_media_misc_test.go @@ -517,6 +517,79 @@ func TestMiscConverters(t *testing.T) { } } +// TestFormatMessageItemResourcesGate verifies the resources block is only +// emitted when extractResources is on; the default path (and back-compat +// wrappers) must never add a resources key. +func TestFormatMessageItemResourcesGate(t *testing.T) { + raw := map[string]interface{}{ + "msg_type": "image", + "message_id": "om_img", + "create_time": "1710500000", + "sender": map[string]interface{}{"id": "ou_sender", "sender_type": "user"}, + "body": map[string]interface{}{"content": `{"image_key":"img_99"}`}, + } + + // Gate off via the back-compat wrapper. + off := FormatMessageItemWithMergePrefetch(raw, nil, nil, nil) + if _, ok := off["resources"]; ok { + t.Fatalf("FormatMessageItemWithMergePrefetch should not emit resources, got %#v", off["resources"]) + } + + // Gate off via plain FormatMessageItem. + plain := FormatMessageItem(raw, nil) + if _, ok := plain["resources"]; ok { + t.Fatalf("FormatMessageItem should not emit resources, got %#v", plain["resources"]) + } + + // Gate on. + on := FormatMessageItemWithMergePrefetchOpts(raw, nil, nil, nil, true) + resources, ok := on["resources"].([]map[string]interface{}) + if !ok || len(resources) != 1 { + t.Fatalf("FormatMessageItemWithMergePrefetchOpts(extract=true) resources = %#v, want 1 ref", on["resources"]) + } + r := resources[0] + if r["message_id"] != "om_img" || r["key"] != "img_99" || r["type"] != "image" { + t.Fatalf("resource ref = %#v, want {om_img,img_99,image}", r) + } + if _, ok := r["local_path"]; ok { + t.Fatalf("extract stage must not set local_path yet, got %#v", r["local_path"]) + } +} + +func TestAudioConverterFileKey(t *testing.T) { + tests := []struct { + name string + raw string + want string + }{ + {name: "key and duration", raw: `{"file_key":"audio_1","duration":3500}`, want: `