fix(media): skip path tag append for JSON content (Feishu cards/posts)

When content is structured JSON (interactive cards, post messages),
injectPathTags now skips the fallback append — only placeholder
replacement is attempted. This prevents corrupting JSON payloads
like {"schema":"2.0",...} with appended [image:/path] tags.

Adds looksLikeJSON() helper and three test cases covering JSON
objects, arrays, and an end-to-end resolveMediaRefs scenario with
Feishu card content.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guoguo 2026-04-29 19:02:14 -07:00
parent 9561476347
commit 77beafa6d3
2 changed files with 55 additions and 0 deletions

View file

@ -242,7 +242,12 @@ func buildPathTag(mime, localPath string) string {
// or appends if no matching generic tag is found. Channels emit a few different
// placeholder formats — [image], [image: photo], [image: filename.jpg] — so we
// match all of them via regex while leaving path tags ([image:/path]) untouched.
//
// When content is structured data (e.g., JSON from Feishu interactive cards or
// post messages), tags are only injected via placeholder replacement — never
// appended — to avoid corrupting the payload.
func injectPathTags(content string, tags []string) string {
isStructured := looksLikeJSON(content)
for _, tag := range tags {
var pattern *regexp.Regexp
switch {
@ -263,6 +268,10 @@ func injectPathTags(content string, tags []string) string {
}
}
if isStructured {
continue
}
if content == "" {
content = tag
} else {
@ -271,3 +280,8 @@ func injectPathTags(content string, tags []string) string {
}
return content
}
func looksLikeJSON(s string) bool {
s = strings.TrimSpace(s)
return len(s) > 1 && (s[0] == '{' || s[0] == '[')
}

View file

@ -5040,6 +5040,47 @@ func TestInjectPathTags_DoesNotReplacePathTag(t *testing.T) {
}
}
func TestInjectPathTags_SkipsAppendForJSONContent(t *testing.T) {
jsonContent := `{"schema":"2.0","body":{"elements":[{"tag":"img","img_key":"img_123"}]}}`
got := injectPathTags(jsonContent, []string{"[image:/tmp/photo.png]"})
if got != jsonContent {
t.Fatalf("expected JSON content unchanged, got %q", got)
}
}
func TestInjectPathTags_JSONArrayContent(t *testing.T) {
jsonContent := `[{"tag":"text","text":"hello"}]`
got := injectPathTags(jsonContent, []string{"[file:/tmp/doc.pdf]"})
if got != jsonContent {
t.Fatalf("expected JSON array content unchanged, got %q", got)
}
}
func TestResolveMediaRefs_JSONContentPreservesStructure(t *testing.T) {
store := media.NewFileMediaStore()
dir := t.TempDir()
pngPath := filepath.Join(dir, "card_img.png")
pngHeader := []byte{
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02,
0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE,
}
os.WriteFile(pngPath, pngHeader, 0o644)
ref, _ := store.Store(pngPath, media.MediaMeta{ContentType: "image/png"}, "test")
jsonContent := `{"schema":"2.0","body":{"elements":[{"tag":"img","img_key":"img_123"}]}}`
messages := []providers.Message{
{Role: "user", Content: jsonContent, Media: []string{ref}},
}
result := resolveMediaRefs(messages, store, config.DefaultMaxMediaSize)
if result[0].Content != jsonContent {
t.Fatalf("expected JSON content preserved, got %q", result[0].Content)
}
}
func TestResolveMediaRefs_EmptyContentGetsPathTag(t *testing.T) {
store := media.NewFileMediaStore()
dir := t.TempDir()