diff --git a/shortcuts/im/convert_lib/helpers_test.go b/shortcuts/im/convert_lib/helpers_test.go index 3220ad6c..078882c7 100644 --- a/shortcuts/im/convert_lib/helpers_test.go +++ b/shortcuts/im/convert_lib/helpers_test.go @@ -122,7 +122,7 @@ func TestExtractPostBlocksText(t *testing.T) { } got := extractPostBlocksText(blocks) - want := "hello @Alice [docs](https://example.com)\n[Image: img_123]" + want := "hello @Alice [docs](https://example.com)\n![Image](img_123)" if got != want { t.Fatalf("extractPostBlocksText() = %q, want %q", got, want) } diff --git a/shortcuts/im/convert_lib/text.go b/shortcuts/im/convert_lib/text.go index 67112f6c..c047be7f 100644 --- a/shortcuts/im/convert_lib/text.go +++ b/shortcuts/im/convert_lib/text.go @@ -39,16 +39,16 @@ func (postConverter) Convert(ctx *ConvertContext) string { if title, _ := body["title"].(string); title != "" { parts = append(parts, title) } - if blocks, _ := body["content"].([]interface{}); len(blocks) > 0 { - for _, para := range blocks { - elems, _ := para.([]interface{}) - var line strings.Builder - for _, el := range elems { - elem, _ := el.(map[string]interface{}) - line.WriteString(renderPostElem(elem)) - } - parts = append(parts, line.String()) + // Prefer content_v2 blocks; fallback to content blocks + blocks := selectContentBlocks(body) + for _, para := range blocks { + elems, _ := para.([]interface{}) + var line strings.Builder + for _, el := range elems { + elem, _ := el.(map[string]interface{}) + line.WriteString(renderPostElem(elem)) } + parts = append(parts, line.String()) } result := strings.TrimSpace(strings.Join(parts, "\n")) @@ -58,6 +58,17 @@ func (postConverter) Convert(ctx *ConvertContext) string { return ResolveMentionKeys(result, ctx.MentionMap) } +// selectContentBlocks returns content_v2 blocks when present and non-empty; +// otherwise falls back to content blocks. This implements the content_v2 +// priority rule for post messages. +func selectContentBlocks(body map[string]interface{}) []interface{} { + if v2, ok := body["content_v2"].([]interface{}); ok && len(v2) > 0 { + return v2 + } + blocks, _ := body["content"].([]interface{}) + return blocks +} + func unwrapPostLocale(parsed map[string]interface{}) map[string]interface{} { if _, ok := parsed["content"]; ok { return parsed @@ -114,10 +125,14 @@ func renderPostElem(el map[string]interface{}) string { var rendered string switch { case userId == "@_all" || userId == "all": - rendered = "@all" + rendered = `` default: if name, _ := el["user_name"].(string); name != "" { - rendered = "@" + name + if userId != "" && strings.HasPrefix(userId, "ou") { + rendered = fmt.Sprintf(`%s`, userId, name) + } else { + rendered = "@" + name + } } else { rendered = "@" + userId } @@ -138,7 +153,7 @@ func renderPostElem(el map[string]interface{}) string { case "img": key, _ := el["image_key"].(string) if key != "" { - return fmt.Sprintf("[Image: %s]", key) + return fmt.Sprintf("![Image](%s)", key) } return "[Image]" case "media": diff --git a/shortcuts/im/convert_lib/text_test.go b/shortcuts/im/convert_lib/text_test.go index 9029ef2c..235b5857 100644 --- a/shortcuts/im/convert_lib/text_test.go +++ b/shortcuts/im/convert_lib/text_test.go @@ -93,9 +93,13 @@ func TestRenderPostElem(t *testing.T) { }{ {name: "text", el: map[string]interface{}{"tag": "text", "text": "hello"}, want: "hello"}, {name: "link", el: map[string]interface{}{"tag": "a", "text": "doc", "href": "https://example.com"}, want: "[doc](https://example.com)"}, - {name: "mention all", el: map[string]interface{}{"tag": "at", "user_id": "@_all"}, want: "@all"}, - {name: "mention user", el: map[string]interface{}{"tag": "at", "user_name": "Alice"}, want: "@Alice"}, - {name: "image", el: map[string]interface{}{"tag": "img", "image_key": "img_123"}, want: "[Image: img_123]"}, + {name: "mention all", el: map[string]interface{}{"tag": "at", "user_id": "@_all"}, want: ``}, + {name: "mention user with id", el: map[string]interface{}{"tag": "at", "user_id": "ou_user_1", "user_name": "Alice"}, want: `Alice`}, + {name: "mention user name only", el: map[string]interface{}{"tag": "at", "user_name": "Alice"}, want: "@Alice"}, + {name: "mention user id only", el: map[string]interface{}{"tag": "at", "user_id": "@_user_1"}, want: "@@_user_1"}, + {name: "image", el: map[string]interface{}{"tag": "img", "image_key": "img_123"}, want: "![Image](img_123)"}, + {name: "image no key", el: map[string]interface{}{"tag": "img"}, want: "[Image]"}, + {name: "md text", el: map[string]interface{}{"tag": "md", "text": "##### 标题\n\nAlice 你好"}, want: "##### 标题\n\nAlice 你好"}, {name: "media", el: map[string]interface{}{"tag": "media", "file_key": "file_123"}, want: "[Media: file_123]"}, {name: "code block", el: map[string]interface{}{"tag": "code_block", "language": "go", "text": "fmt.Println(1)"}, want: "\n```go\nfmt.Println(1)\n```\n"}, {name: "hr", el: map[string]interface{}{"tag": "hr"}, want: "\n---\n"}, @@ -144,3 +148,87 @@ func TestRenderPostElemEmotionStyleMd(t *testing.T) { }) } } + +func TestSelectContentBlocks(t *testing.T) { + tests := []struct { + name string + body map[string]interface{} + want int + }{ + { + name: "content_v2 present and non-empty", + body: map[string]interface{}{ + "content": []interface{}{[]interface{}{map[string]interface{}{"tag": "text", "text": "old"}}}, + "content_v2": []interface{}{[]interface{}{map[string]interface{}{"tag": "md", "text": "new"}}}, + }, + want: 1, + }, + { + name: "content_v2 empty array", + body: map[string]interface{}{ + "content": []interface{}{[]interface{}{map[string]interface{}{"tag": "text", "text": "old"}}}, + "content_v2": []interface{}{}, + }, + want: 1, + }, + { + name: "content_v2 nil", + body: map[string]interface{}{ + "content": []interface{}{[]interface{}{map[string]interface{}{"tag": "text", "text": "old"}}}, + }, + want: 1, + }, + { + name: "content_v2 wrong type", + body: map[string]interface{}{ + "content": []interface{}{[]interface{}{map[string]interface{}{"tag": "text", "text": "old"}}}, + "content_v2": "not_an_array", + }, + want: 1, + }, + { + name: "both missing", + body: map[string]interface{}{}, + want: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := selectContentBlocks(tt.body) + if len(got) != tt.want { + t.Fatalf("selectContentBlocks() len = %d, want %d", len(got), tt.want) + } + }) + } +} + +func TestPostConverterConvertContentV2(t *testing.T) { + // AC-M1-H1: content_v2 present → use content_v2 blocks (md passthrough) + ctx := &ConvertContext{ + RawContent: `{"content_v2":[[{"tag":"md","text":"##### 标题\n\nAlice 你好"}]],"content":[[{"tag":"text","text":"old path"}]]}`, + } + want := "##### 标题\n\nAlice 你好" + if got := (postConverter{}).Convert(ctx); got != want { + t.Fatalf("postConverter.Convert(content_v2) = %q, want %q", got, want) + } + + // AC-M1-H2: no content_v2 → use content blocks with new at/img format + ctx2 := &ConvertContext{ + RawContent: `{"content":[[{"tag":"at","user_id":"ou_xxx","user_name":"Bob"},{"tag":"text","text":" "},{"tag":"img","image_key":"img_123"}]]}`, + Mentions: []interface{}{map[string]interface{}{"key": "ou_xxx", "id": "ou_bob", "name": "Bob"}}, + } + want2 := `Bob ![Image](img_123)` + if got := (postConverter{}).Convert(ctx2); got != want2 { + t.Fatalf("postConverter.Convert(content) = %q, want %q", got, want2) + } + + // AC-M1-E1: content_v2 empty → fallback to content + ctx3 := &ConvertContext{ + RawContent: `{"content_v2":[],"content":[[{"tag":"text","text":"fallback path"}]]}`, + } + want3 := "fallback path" + if got := (postConverter{}).Convert(ctx3); got != want3 { + t.Fatalf("postConverter.Convert(empty content_v2) = %q, want %q", got, want3) + } +} diff --git a/skills/lark-im/references/lark-im-chat-messages-list.md b/skills/lark-im/references/lark-im-chat-messages-list.md index 8e1aea14..d0f5af18 100644 --- a/skills/lark-im/references/lark-im-chat-messages-list.md +++ b/skills/lark-im/references/lark-im-chat-messages-list.md @@ -53,7 +53,7 @@ lark-cli im +chat-messages-list --chat-id oc_xxx --format json ## Resource Rendering -Messages are rendered into human-readable text for inspection. Image messages are shown as placeholders such as `[Image: img_xxx]`; files, audio, and videos are rendered with resource keys in the content (e.g. `