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"
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("", 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: ""},
+ {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 `
+ 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. ``). By default resource binaries are **not** downloaded.
+Messages are rendered into human-readable text for inspection. Image messages are shown as placeholders such as ``; files, audio, and videos are rendered with resource keys in the content (e.g. ``). By default resource binaries are **not** downloaded.
Two ways to get the binaries:
- **In one pass:** add `--download-resources` to this command — every eligible resource (image/file/audio/video/media + post-embedded, excluding stickers) is downloaded into `./lark-im-resources/` and a `resources` block (`{message_id, key, type, local_path, size_bytes}`) is attached to each message. See [message enrichment](lark-im-message-enrichment.md#resource-auto-download---download-resources-opt-in).
@@ -61,7 +61,7 @@ Two ways to get the binaries:
| Resource Type | Marker in Content | Behavior |
|---------|-------------|------|
-| Image | `[Image: img_xxx]` | `--download-resources`, or manually `im +messages-resources-download --type image` |
+| Image | `` | `--download-resources`, or manually `im +messages-resources-download --type image` |
| File | `` | `--download-resources`, or manually `im +messages-resources-download --type file` |
| Audio | `` | `--download-resources`, or manually `im +messages-resources-download --type file` |
| Video | `` | `--download-resources`, or manually `im +messages-resources-download --type file` |
diff --git a/skills/lark-im/references/lark-im-message-enrichment.md b/skills/lark-im/references/lark-im-message-enrichment.md
index 644f081c..da0ad075 100644
--- a/skills/lark-im/references/lark-im-message-enrichment.md
+++ b/skills/lark-im/references/lark-im-message-enrichment.md
@@ -32,7 +32,7 @@ When enabled:
- Output paths are confined to `./lark-im-resources/` by the same guards as [`+messages-resources-download`](lark-im-messages-resources-download.md) (abnormal `file_key` with path separators / `..` / absolute paths is rejected).
- **Scope**: the download uses `GET /open-apis/im/v1/messages/:message_id/resources/:file_key`, which requires `im:message:readonly` — already declared in each listing command's `Scopes`, so `--download-resources` needs **no extra scope** beyond what's required to read the messages (user identity also needs `im:message.group_msg:get_as_user` / `im:message.p2p_msg:get_as_user`; bot identity needs `im:message.group_msg` / `im:message.p2p_msg:readonly`, all already declared). Works under both user and bot identity. If a bot was registered before `im:message:readonly` was granted, a single resource will fail-silently (`error: true` + stderr warning) rather than aborting the pull.
-Use `--download-resources` when you want the binaries on disk in one pass; otherwise the message content keeps the inline resource markers (e.g. `[Image: img_xxx]`, ``, ``) and you can fetch individual resources later with [`+messages-resources-download`](lark-im-messages-resources-download.md).
+Use `--download-resources` when you want the binaries on disk in one pass; otherwise the message content keeps the inline resource markers (e.g. ``, ``, ``) and you can fetch individual resources later with [`+messages-resources-download`](lark-im-messages-resources-download.md).
## Scope requirement
diff --git a/skills/lark-im/references/lark-im-messages-mget.md b/skills/lark-im/references/lark-im-messages-mget.md
index ac30a55f..852efcea 100644
--- a/skills/lark-im/references/lark-im-messages-mget.md
+++ b/skills/lark-im/references/lark-im-messages-mget.md
@@ -90,7 +90,7 @@ lark-cli im +messages-mget --message-ids "om_aaa,om_bbb"
1. **Use JSON for full content:** table output truncates content. Use `--format json` when the full body matters.
2. **Sender names are already enriched:** the command resolves sender names automatically, so no extra lookup is required.
-3. **Images are rendered as placeholders:** image messages appear as placeholders such as `[Image: img_xxx]`. Use `+messages-resources-download` when you need the binary resource.
+3. **Images are rendered as placeholders:** image messages appear as placeholders such as ``. Use `+messages-resources-download` when you need the binary resource.
4. **Batching is more efficient:** fetching multiple IDs in one request is better than calling the API repeatedly.
## References
diff --git a/skills/lark-im/references/lark-im-messages-search.md b/skills/lark-im/references/lark-im-messages-search.md
index 594ecca3..b466d541 100644
--- a/skills/lark-im/references/lark-im-messages-search.md
+++ b/skills/lark-im/references/lark-im-messages-search.md
@@ -152,7 +152,7 @@ lark-cli im +threads-messages-list --thread
## Resource Rendering
-Search results reuse the same content formatter as other read commands. Image messages are rendered as placeholders such as `[Image: img_xxx]`; resource binaries are **not** downloaded automatically.
+Search results reuse the same content formatter as other read commands. Image messages are rendered as placeholders such as ``; resource binaries are **not** downloaded automatically.
Use `im +messages-resources-download` if you need to fetch the underlying image or file bytes from a specific message.
diff --git a/skills/lark-im/references/lark-im-threads-messages-list.md b/skills/lark-im/references/lark-im-threads-messages-list.md
index 24ddcba2..61c4acec 100644
--- a/skills/lark-im/references/lark-im-threads-messages-list.md
+++ b/skills/lark-im/references/lark-im-threads-messages-list.md
@@ -96,7 +96,7 @@ lark-cli im +threads-messages-list --thread omt_xxx --page-token
## Resource Rendering
-Thread replies are rendered into human-readable text. Image messages appear as placeholders such as `[Image: img_xxx]`; by default resource binaries are **not** downloaded.
+Thread replies are rendered into human-readable text. Image messages appear as placeholders such as ``; by default resource binaries are **not** downloaded.
Pass `--download-resources` to download every eligible resource (image/file/audio/video/media + post-embedded, excluding stickers) into `./lark-im-resources/` in one pass and attach a `resources` block to each reply (see [message enrichment](lark-im-message-enrichment.md#resource-auto-download---download-resources-opt-in)). Otherwise download individual resources manually through `im +messages-resources-download` (see [lark-im-messages-resources-download](lark-im-messages-resources-download.md)).