From 4686676405c01e7d9ad643708e9468de87eeb433 Mon Sep 17 00:00:00 2001 From: "liujiashu.shiro" Date: Fri, 8 May 2026 15:34:14 +0800 Subject: [PATCH 1/2] feat(im): support Markdown image rendering in post content Leverage underlying URL compatibility and remove redundant URL conversion logic. Add the ability to send Markdown images. --- shortcuts/im/helpers.go | 191 +----------------- shortcuts/im/helpers_test.go | 172 +--------------- .../references/lark-im-messages-reply.md | 26 ++- .../references/lark-im-messages-send.md | 26 ++- 4 files changed, 68 insertions(+), 347 deletions(-) diff --git a/shortcuts/im/helpers.go b/shortcuts/im/helpers.go index 5b42c32b7..a49e3b950 100644 --- a/shortcuts/im/helpers.go +++ b/shortcuts/im/helpers.go @@ -911,12 +911,16 @@ func marshalMarkdownPostContent(content [][]map[string]interface{}) string { "content": content, }, } - return marshalJSONNoEscape(payload) + data, _ := json.Marshal(payload) + return string(data) } func buildSingleMDPost(markdown string) string { return marshalMarkdownPostContent([][]map[string]interface{}{ - buildPostElementNodes(optimizeMarkdownStyle(markdown)), + {{ + "tag": "md", + "text": optimizeMarkdownStyle(markdown), + }}, }) } @@ -940,7 +944,10 @@ func buildSegmentedPost(markdown string) string { if optimized == "" { continue } - content = append(content, buildPostElementNodes(optimized)) + content = append(content, []map[string]interface{}{{ + "tag": "md", + "text": optimized, + }}) } if len(content) == 0 { return buildSingleMDPost(markdown) @@ -955,186 +962,8 @@ func buildMarkdownPostContent(markdown string) string { return buildSingleMDPost(markdown) } -// buildPostElementNodes splits optimized markdown text into Feishu post inline -// elements. It tokenizes markdown links/images and bare http(s) URLs: -// - markdown links are kept verbatim inside a {"tag":"md"} segment -// - bare URLs become {"tag":"a"} elements rendered natively by Feishu, -// avoiding the md renderer misinterpreting underscores as italic markers -// -// Fenced code blocks are protected before tokenization so their content remains -// a single md segment, and bare URLs support balanced parentheses in the path. -func buildPostElementNodes(text string) []map[string]interface{} { - protected, codeBlocks := protectMarkdownCodeBlocks(text) - if protected == "" { - return []map[string]interface{}{{ - "tag": "md", - "text": text, - }} - } - elems := make([]map[string]interface{}, 0, 4) - prev := 0 - for i := 0; i < len(protected); { - end, kind, ok := scanPostToken(protected, i) - if !ok { - i++ - continue - } - if i > prev { - elems = appendMDPostNode(elems, restoreMarkdownCodeBlocks(protected[prev:i], codeBlocks)) - } - - token := protected[i:end] - if kind == postTokenMarkdown { - elems = appendMDPostNode(elems, restoreMarkdownCodeBlocks(token, codeBlocks)) - } else { - url := trimBareURLToken(token) - if url == "" { - url = token - } - elems = append(elems, map[string]interface{}{ - "tag": "a", - "text": url, - "href": url, - }) - elems = appendMDPostNode(elems, restoreMarkdownCodeBlocks(token[len(url):], codeBlocks)) - } - prev = end - i = end - } - if prev < len(protected) { - elems = appendMDPostNode(elems, restoreMarkdownCodeBlocks(protected[prev:], codeBlocks)) - } - if len(elems) == 0 { - return []map[string]interface{}{{ - "tag": "md", - "text": text, - }} - } - return elems -} - -func trimBareURLToken(token string) string { - trimmed := strings.TrimRight(token, ".,;:!?") - for strings.HasSuffix(trimmed, ")") && strings.Count(trimmed, "(") < strings.Count(trimmed, ")") { - trimmed = strings.TrimSuffix(trimmed, ")") - } - return trimmed -} - -type postTokenKind int - -const ( - postTokenMarkdown postTokenKind = iota - postTokenURL -) - -func appendMDPostNode(elems []map[string]interface{}, text string) []map[string]interface{} { - if text == "" { - return elems - } - return append(elems, map[string]interface{}{ - "tag": "md", - "text": text, - }) -} - -func scanPostToken(text string, start int) (end int, kind postTokenKind, ok bool) { - if end, ok = scanMarkdownLinkToken(text, start); ok { - return end, postTokenMarkdown, true - } - if end, ok = scanBareURLToken(text, start); ok { - return end, postTokenURL, true - } - return 0, 0, false -} - -func scanMarkdownLinkToken(text string, start int) (int, bool) { - openBracket := start - if text[start] == '!' { - if start+1 >= len(text) || text[start+1] != '[' { - return 0, false - } - openBracket = start + 1 - } else if text[start] != '[' { - return 0, false - } - - closeBracket := strings.IndexByte(text[openBracket+1:], ']') - if closeBracket < 0 { - return 0, false - } - closeBracket += openBracket + 1 - if closeBracket+1 >= len(text) || text[closeBracket+1] != '(' { - return 0, false - } - return scanBalancedParenToken(text, closeBracket+1) -} - -func scanBareURLToken(text string, start int) (int, bool) { - if !strings.HasPrefix(text[start:], "http://") && !strings.HasPrefix(text[start:], "https://") { - return 0, false - } - - depth := 0 - for i := start; i < len(text); i++ { - switch text[i] { - case ' ', '\t', '\n', '\r', '<', '>', '"', '[', ']': - return i, i > start - case '(': - depth++ - case ')': - if depth == 0 { - return i, i > start - } - depth-- - } - } - return len(text), true -} - -func scanBalancedParenToken(text string, openParen int) (int, bool) { - if openParen >= len(text) || text[openParen] != '(' { - return 0, false - } - - depth := 0 - for i := openParen; i < len(text); i++ { - switch text[i] { - case '(': - depth++ - case ')': - depth-- - if depth == 0 { - return i + 1, true - } - } - } - return 0, false -} - -func buildPostElements(text string) string { - return marshalJSONNoEscape(buildPostElementNodes(text)) -} - -func marshalJSONNoEscape(v interface{}) string { - var buf bytes.Buffer - enc := json.NewEncoder(&buf) - enc.SetEscapeHTML(false) - _ = enc.Encode(v) - return strings.TrimSuffix(buf.String(), "\n") -} - -// marshalStringNoEscape serializes a string to JSON without HTML-escaping -// special characters like &, <, >. Go's json.Marshal escapes them to \u0026 -// etc. by default, which breaks URLs containing & in Feishu's md renderer. -func marshalStringNoEscape(s string) string { - return marshalJSONNoEscape(s) -} - // wrapMarkdownAsPost wraps markdown text into Feishu post format JSON (no network). // Used by DryRun. Output may include md/text paragraphs when blank-line separators are present. -// Bare URLs are emitted as {"tag":"a"} elements to avoid Feishu's md renderer -// misinterpreting underscores in URLs as italic markers. func wrapMarkdownAsPost(markdown string) string { return buildMarkdownPostContent(markdown) } diff --git a/shortcuts/im/helpers_test.go b/shortcuts/im/helpers_test.go index 853d5e9f0..4776b988e 100644 --- a/shortcuts/im/helpers_test.go +++ b/shortcuts/im/helpers_test.go @@ -373,173 +373,21 @@ func TestOptimizeMarkdownStyle(t *testing.T) { } } -func TestMarshalStringNoEscape(t *testing.T) { - tests := []struct { - name string - input string - want string - }{ - {name: "ampersand not escaped", input: "a=1&b=2", want: `"a=1&b=2"`}, - {name: "angle brackets not escaped", input: "", want: `""`}, - {name: "regular string", input: "hello world", want: `"hello world"`}, - {name: "url with ampersand", input: "https://example.com?a=1&b=2", want: `"https://example.com?a=1&b=2"`}, +func TestWrapMarkdownAsPost(t *testing.T) { + got := wrapMarkdownAsPost("hello **world**") + content := decodePostContentForTest(t, got) + if len(content) != 1 { + t.Fatalf("wrapMarkdownAsPost() content len = %d, want 1", len(content)) } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := marshalStringNoEscape(tt.input) - if got != tt.want { - t.Errorf("marshalStringNoEscape(%q) = %q, want %q", tt.input, got, tt.want) - } - }) + node := decodePostParagraphForTest(t, got, 0) + if node["tag"] != "md" { + t.Fatalf("wrapMarkdownAsPost() tag = %#v, want md", node["tag"]) } -} - -func TestBuildPostElements(t *testing.T) { - tests := []struct { - name string - input string - wantSubs []string // substrings that must appear - wantNsubs []string // substrings that must NOT appear - }{ - { - name: "plain text no URL", - input: "hello **world**", - wantSubs: []string{`"tag":"md"`, `hello **world**`}, - }, - { - name: "bare URL only", - input: "https://example.com/path", - wantSubs: []string{`"tag":"a"`, `"text":"https://example.com/path"`, `"href":"https://example.com/path"`}, - }, - { - name: "bare URL with underscores", - input: "https://example.com/flow_id=abc_def", - wantSubs: []string{`"tag":"a"`, `flow_id=abc_def`}, - }, - { - name: "bare URL with ampersand not escaped", - input: "https://example.com?a=1&b=2", - wantSubs: []string{`"tag":"a"`, `a=1&b=2`}, - }, - { - name: "text before and after URL", - input: "click here: https://example.com/path ok?", - wantSubs: []string{`"tag":"md"`, `click here: `, `"tag":"a"`, `https://example.com/path`, ` ok?`}, - }, - { - name: "markdown link kept in md segment", - input: "[click here](https://example.com/path_with_underscore)", - wantSubs: []string{`"tag":"md"`, `[click here](https://example.com/path_with_underscore)`}, - }, - { - name: "markdown link not promoted to a tag", - input: "[text](https://example.com)", - wantSubs: []string{`"tag":"md"`}, - wantNsubs: []string{`"tag":"a"`}, - }, - { - name: "multiple bare URLs", - input: "https://a.com/x_y and https://b.com/p_q", - wantSubs: []string{ - `"tag":"a"`, `https://a.com/x_y`, - `https://b.com/p_q`, - `"tag":"md"`, ` and `, - }, - }, - { - name: "mixed markdown and bare URL", - input: "**bold** https://example.com/foo_bar [link](https://example.com) end", - wantSubs: []string{`"tag":"md"`, `**bold**`, `"tag":"a"`, `foo_bar`, `[link](https://example.com)`}, - }, - { - name: "empty string", - input: "", - wantSubs: []string{`"tag":"md"`, `"text":""`}, - }, - { - name: "URL followed by comma", - input: "visit https://example.com/path, then click", - wantSubs: []string{`"tag":"a"`, `"href":"https://example.com/path"`}, - wantNsubs: []string{`https://example.com/path,`}, - }, - { - name: "URL followed by period", - input: "see https://example.com/foo.", - wantSubs: []string{`"tag":"a"`, `https://example.com/foo`}, - wantNsubs: []string{`https://example.com/foo."`}, - }, - { - name: "URL with no trailing punctuation unchanged", - input: "https://example.com/foo_bar", - wantSubs: []string{`"href":"https://example.com/foo_bar"`}, - }, - { - name: "URL with balanced parentheses preserved", - input: "https://en.wikipedia.org/wiki/Foo_(bar)", - wantSubs: []string{`"href":"https://en.wikipedia.org/wiki/Foo_(bar)"`}, - wantNsubs: []string{`"href":"https://en.wikipedia.org/wiki/Foo_"`}, - }, - { - name: "code block URL stays markdown", - input: "```bash\ncurl https://example.com/foo_bar\n```", - wantSubs: []string{`"tag":"md"`, "```bash\\ncurl https://example.com/foo_bar\\n```"}, - wantNsubs: []string{`"tag":"a"`}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := buildPostElements(tt.input) - for _, sub := range tt.wantSubs { - if !strings.Contains(got, sub) { - t.Errorf("buildPostElements(%q)\n got: %s\n missing: %q", tt.input, got, sub) - } - } - for _, sub := range tt.wantNsubs { - if strings.Contains(got, sub) { - t.Errorf("buildPostElements(%q)\n got: %s\n should not contain: %q", tt.input, got, sub) - } - } - }) + if node["text"] != "hello **world**" { + t.Fatalf("wrapMarkdownAsPost() text = %#v, want %q", node["text"], "hello **world**") } } -func TestWrapMarkdownAsPost(t *testing.T) { - t.Run("plain markdown", func(t *testing.T) { - got := wrapMarkdownAsPost("hello **world**") - content := decodePostContentForTest(t, got) - if len(content) != 1 { - t.Fatalf("wrapMarkdownAsPost() content len = %d, want 1", len(content)) - } - node := decodePostParagraphForTest(t, got, 0) - if node["tag"] != "md" { - t.Fatalf("wrapMarkdownAsPost() tag = %#v, want md", node["tag"]) - } - if node["text"] != "hello **world**" { - t.Fatalf("wrapMarkdownAsPost() text = %#v, want %q", node["text"], "hello **world**") - } - }) - - t.Run("bare URL becomes a tag", func(t *testing.T) { - got := wrapMarkdownAsPost("see https://example.com/flow_id=abc_def done") - if !strings.Contains(got, `"tag":"a"`) { - t.Fatalf("wrapMarkdownAsPost() bare URL should produce a tag: %s", got) - } - if !strings.Contains(got, `flow_id=abc_def`) { - t.Fatalf("wrapMarkdownAsPost() URL content missing: %s", got) - } - }) - - t.Run("code block URL stays md", func(t *testing.T) { - got := wrapMarkdownAsPost("```bash\ncurl https://example.com/foo_bar\n```") - if strings.Contains(got, `"tag":"a"`) { - t.Fatalf("wrapMarkdownAsPost() code block URL should stay markdown: %s", got) - } - if !strings.Contains(got, "```bash\\ncurl https://example.com/foo_bar\\n```") { - t.Fatalf("wrapMarkdownAsPost() code block content missing: %s", got) - } - }) -} - func TestShouldUseSegmentedPost(t *testing.T) { tests := []struct { name string diff --git a/skills/lark-im/references/lark-im-messages-reply.md b/skills/lark-im/references/lark-im-messages-reply.md index cc73a5993..34fa8b07c 100644 --- a/skills/lark-im/references/lark-im-messages-reply.md +++ b/skills/lark-im/references/lark-im-messages-reply.md @@ -64,12 +64,27 @@ So `--markdown` is a convenience mode, not a full Markdown compatibility layer. - Block spacing and line breaks may be normalized during conversion. - Code blocks are preserved as code blocks. - Excess blank lines are compressed. -- Only remote `http://...`, `https://...`, or already-uploaded `img_xxx` Markdown images are kept reliably. -- Local paths in Markdown image syntax like `![x](./a.png)` are **not** auto-uploaded by `--markdown`. +- Only already-uploaded `img_xxx` Markdown images are kept reliably. +- Local paths (e.g. `![x](./a.png)`) and remote URLs are **not** supported as image links — all images must be downloaded and uploaded via `images.create`, provided as an `image_key`. - If remote Markdown image handling fails, that image is removed with a warning. If you need exact output, use `--msg-type post --content ...` instead of `--markdown`. +### Image Constraint for `--markdown` + +When using `--markdown` and the reply content includes images, you **must** first upload the image via `images.create` to obtain an `image_key`, then reference it as `![alt](img_xxx)`. + +**Steps:** + +```bash +# 1. Upload image to get image_key +lark-cli im images create --data '{"image_type":"message"}' --file ./diagram.png +# Returns: {"image_key":"img_v3_xxxx"} + +# 2. Use image_key in --markdown reply +lark-cli im +messages-reply --message-id om_xxx --markdown $'## Result\n\n![diagram](img_v3_xxxx)\n\nSee above for details.' +``` + ## Preserving Formatting If the reply contains multiple lines, code blocks, indentation, tabs, or a lot of escaping, prefer `$'...'`. @@ -119,6 +134,11 @@ lark-cli im +messages-reply --message-id om_xxx --text "Let's discuss this" --re # Reply with basic Markdown (will be converted to post JSON) lark-cli im +messages-reply --message-id om_xxx --markdown $'## Reply\n\n- item 1\n- item 2' +# Reply with Markdown containing an image (must pre-upload via images.create) +lark-cli im images create --data '{"image_type":"message"}' --file ./screenshot.png +# Use the returned image_key +lark-cli im +messages-reply --message-id om_xxx --markdown $'## Screenshot\n\n![screenshot](img_v3_xxxx)\n\nConfirmed.' + # If you need exact post structure, send JSON directly lark-cli im +messages-reply --message-id om_xxx --msg-type post --content '{"zh_cn":{"title":"Reply","content":[[{"tag":"text","text":"Detailed content"}]]}}' @@ -166,6 +186,7 @@ lark-cli im +messages-reply --message-id om_xxx --markdown $'## Test\n\nhello' - - Choosing `--markdown` when you actually need exact plain text. If exact line breaks and spacing matter, use `--text`, usually with `$'...'`. - Assuming `--markdown` supports all Markdown features. It does not; it is converted into a Feishu `post` payload and rewritten first. - Putting local image paths inside Markdown like `![x](./a.png)`. `--markdown` does not auto-upload those paths. +- **Using `--markdown` with images without first uploading via `images.create`.** All images must be pre-uploaded to obtain an `image_key`. Neither local paths nor remote URLs can be used directly — otherwise the image will be replaced with placeholder text. - Using `--content` without making the JSON match the effective `--msg-type`. - Explicitly setting `--msg-type` to something that conflicts with `--text`, `--markdown`, or media flags. - Mixing `--text`, `--markdown`, or `--content` with media flags in one command. @@ -220,3 +241,4 @@ The reply appears in the target message's thread and does not show up in the mai - Failures return error codes and messages - `--as user` uses a user access token (UAT) and requires the `im:message.send_as_user` and `im:message` scopes; the reply is sent as the authorized end user - `--as bot` uses a tenant access token (TAT), and requires the `im:message:send_as_bot` scope +- When using `--markdown` with images, all images must be uploaded via `images.create` first to obtain an `image_key`; local paths and remote URLs are not supported as image links directly diff --git a/skills/lark-im/references/lark-im-messages-send.md b/skills/lark-im/references/lark-im-messages-send.md index a328063a3..5e71da62a 100644 --- a/skills/lark-im/references/lark-im-messages-send.md +++ b/skills/lark-im/references/lark-im-messages-send.md @@ -64,12 +64,27 @@ This means `--markdown` is convenient, but it is not a full-fidelity Markdown tr - Block spacing and line breaks may be normalized during conversion. - Code blocks are preserved as code blocks. - Excess blank lines are compressed. -- Only `http://...`, `https://...`, or already-uploaded `img_xxx` Markdown images are kept reliably. -- Local paths in Markdown image syntax like `![x](./a.png)` are **not** auto-uploaded by `--markdown`; they may be stripped during optimization. +- Only already-uploaded `img_xxx` Markdown images are kept reliably. +- Local paths in Markdown image syntax like `![x](./a.png)` are **not** supported. - If remote Markdown image download/upload fails, that image is removed with a warning. If any of the above is unacceptable, do **not** use `--markdown`; use `--content` and provide the final JSON yourself. +### Image Constraint for `--markdown` + +When using `--markdown` and the message content includes images, you **must** first upload the image via `images.create` to obtain an `image_key`, then reference it as `![alt](img_xxx)`. + +**Steps:** + +```bash +# 1. Upload image to get image_key +lark-cli im images create --data '{"image_type":"message"}' --file ./diagram.png +# Returns: {"image_key":"img_v3_xxxx"} + +# 2. Use image_key in --markdown +lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Report\n\n![diagram](img_v3_xxxx)\n\nSee above for details.' +``` + ## Preserving Formatting If the message has multiple lines, indentation, code blocks, tabs, or many quotes/backslashes, prefer shell ANSI-C quoting with `$'...'`. @@ -118,6 +133,11 @@ lark-cli im +messages-send --chat-id oc_xxx --text $'Line 1\nLine 2\n indented # Send basic Markdown (will be converted to post JSON) lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Update\n\n- item 1\n- item 2' +# Send Markdown with an image (must pre-upload via images.create) +lark-cli im images create --data '{"image_type":"message"}' --file ./screenshot.png +# Use the returned image_key in the markdown content +lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Status\n\n![screenshot](img_v3_xxxx)\n\nDone.' + # If you need exact post structure, send JSON directly lark-cli im +messages-send --chat-id oc_xxx --msg-type post --content '{"zh_cn":{"title":"Title","content":[[{"tag":"text","text":"Body"}]]}}' @@ -172,6 +192,7 @@ lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Test\n\nhello' --dry - Choosing `--markdown` when you actually need exact plain text. If exact line breaks and spacing matter, use `--text`, usually with `$'...'`. - Assuming `--markdown` supports all Markdown features. It does not; it is converted into a Feishu `post` payload and rewritten first. - Putting local image paths inside Markdown like `![x](./a.png)`. `--markdown` does not auto-upload those paths. +- **Using `--markdown` with images without first uploading via `images.create`.** All images must be pre-uploaded to obtain an `image_key`. Neither local paths nor remote URLs can be used directly — otherwise the image will be replaced with placeholder text. - Using `--content` without making the JSON match the effective `--msg-type`. - Explicitly setting `--msg-type` to something that conflicts with `--text`, `--markdown`, or media flags. - Mixing `--text`, `--markdown`, or `--content` with media flags in one command. @@ -221,3 +242,4 @@ lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Test\n\nhello' --dry - `--as user` uses a user access token (UAT) and requires the `im:message.send_as_user` and `im:message` scopes; the message is sent as the authorized end user - `--as bot` uses a tenant access token (TAT) and requires the `im:message:send_as_bot` scope - When sending as a bot, the app must already be in the target group or already have a direct-message relationship with the target user +- When using `--markdown` with images, all images must be uploaded via `images.create` first to obtain an `image_key`; local paths and remote URLs are not supported as image links directly From ec827b6ccf1cfadb3edcc25bfab8d7bb33b72593 Mon Sep 17 00:00:00 2001 From: "liujiashu.shiro" Date: Thu, 14 May 2026 15:55:41 +0800 Subject: [PATCH 2/2] feat(im): align Markdown image guidance with runtime behavior Clarify that remote URLs are auto-resolved at runtime and only removed on failure, rather than being unsupported. Recommend pre-uploading via images.create for reliability. Update caveats, common mistakes, and notes in both messages-send and messages-reply references. --- skills/lark-im/references/lark-im-messages-reply.md | 12 ++++++------ skills/lark-im/references/lark-im-messages-send.md | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/skills/lark-im/references/lark-im-messages-reply.md b/skills/lark-im/references/lark-im-messages-reply.md index 34fa8b07c..cd10260f5 100644 --- a/skills/lark-im/references/lark-im-messages-reply.md +++ b/skills/lark-im/references/lark-im-messages-reply.md @@ -64,15 +64,15 @@ So `--markdown` is a convenience mode, not a full Markdown compatibility layer. - Block spacing and line breaks may be normalized during conversion. - Code blocks are preserved as code blocks. - Excess blank lines are compressed. -- Only already-uploaded `img_xxx` Markdown images are kept reliably. -- Local paths (e.g. `![x](./a.png)`) and remote URLs are **not** supported as image links — all images must be downloaded and uploaded via `images.create`, provided as an `image_key`. -- If remote Markdown image handling fails, that image is removed with a warning. +- Already-uploaded `img_xxx` image keys are the most reliable Markdown image input. +- Local paths (e.g. `![x](./a.png)`) are **not** supported directly in `--markdown` and will not be auto-uploaded. +- Remote URLs (`https://...`) will be auto-downloaded and uploaded at runtime; if the download or upload fails, the image is removed with a warning. If you need exact output, use `--msg-type post --content ...` instead of `--markdown`. ### Image Constraint for `--markdown` -When using `--markdown` and the reply content includes images, you **must** first upload the image via `images.create` to obtain an `image_key`, then reference it as `![alt](img_xxx)`. +When using `--markdown` with images, prefer pre-uploading via `images.create` and referencing `![alt](img_xxx)` for predictable results. Remote URLs may work but are not guaranteed. **Steps:** @@ -186,7 +186,7 @@ lark-cli im +messages-reply --message-id om_xxx --markdown $'## Test\n\nhello' - - Choosing `--markdown` when you actually need exact plain text. If exact line breaks and spacing matter, use `--text`, usually with `$'...'`. - Assuming `--markdown` supports all Markdown features. It does not; it is converted into a Feishu `post` payload and rewritten first. - Putting local image paths inside Markdown like `![x](./a.png)`. `--markdown` does not auto-upload those paths. -- **Using `--markdown` with images without first uploading via `images.create`.** All images must be pre-uploaded to obtain an `image_key`. Neither local paths nor remote URLs can be used directly — otherwise the image will be replaced with placeholder text. +- **Using local file paths inside Markdown image syntax** (e.g. `![x](./a.png)`) with `--markdown`. Local paths are not auto-uploaded and will not render as an image. Pre-upload via `images.create` to get an `image_key` instead. - Using `--content` without making the JSON match the effective `--msg-type`. - Explicitly setting `--msg-type` to something that conflicts with `--text`, `--markdown`, or media flags. - Mixing `--text`, `--markdown`, or `--content` with media flags in one command. @@ -241,4 +241,4 @@ The reply appears in the target message's thread and does not show up in the mai - Failures return error codes and messages - `--as user` uses a user access token (UAT) and requires the `im:message.send_as_user` and `im:message` scopes; the reply is sent as the authorized end user - `--as bot` uses a tenant access token (TAT), and requires the `im:message:send_as_bot` scope -- When using `--markdown` with images, all images must be uploaded via `images.create` first to obtain an `image_key`; local paths and remote URLs are not supported as image links directly +- When using `--markdown` with images, pre-uploading via `images.create` to obtain an `image_key` is recommended for reliability; remote URLs may be auto-resolved at runtime, but if download/upload fails the image is removed with a warning; local paths are not supported diff --git a/skills/lark-im/references/lark-im-messages-send.md b/skills/lark-im/references/lark-im-messages-send.md index 5e71da62a..4f5be8497 100644 --- a/skills/lark-im/references/lark-im-messages-send.md +++ b/skills/lark-im/references/lark-im-messages-send.md @@ -64,15 +64,15 @@ This means `--markdown` is convenient, but it is not a full-fidelity Markdown tr - Block spacing and line breaks may be normalized during conversion. - Code blocks are preserved as code blocks. - Excess blank lines are compressed. -- Only already-uploaded `img_xxx` Markdown images are kept reliably. -- Local paths in Markdown image syntax like `![x](./a.png)` are **not** supported. -- If remote Markdown image download/upload fails, that image is removed with a warning. +- Already-uploaded `img_xxx` image keys are the most reliable Markdown image input. +- Local paths in Markdown image syntax like `![x](./a.png)` are **not** supported and will not be auto-uploaded. +- Remote URLs (`https://...`) will be auto-downloaded and uploaded at runtime; if the download or upload fails, the image is removed with a warning. If any of the above is unacceptable, do **not** use `--markdown`; use `--content` and provide the final JSON yourself. ### Image Constraint for `--markdown` -When using `--markdown` and the message content includes images, you **must** first upload the image via `images.create` to obtain an `image_key`, then reference it as `![alt](img_xxx)`. +When using `--markdown` with images, prefer pre-uploading via `images.create` and referencing `![alt](img_xxx)` for predictable results. Remote URLs may work but are not guaranteed. **Steps:** @@ -192,7 +192,7 @@ lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Test\n\nhello' --dry - Choosing `--markdown` when you actually need exact plain text. If exact line breaks and spacing matter, use `--text`, usually with `$'...'`. - Assuming `--markdown` supports all Markdown features. It does not; it is converted into a Feishu `post` payload and rewritten first. - Putting local image paths inside Markdown like `![x](./a.png)`. `--markdown` does not auto-upload those paths. -- **Using `--markdown` with images without first uploading via `images.create`.** All images must be pre-uploaded to obtain an `image_key`. Neither local paths nor remote URLs can be used directly — otherwise the image will be replaced with placeholder text. +- **Using local file paths inside Markdown image syntax** (e.g. `![x](./a.png)`) with `--markdown`. Local paths are not auto-uploaded and will not render as an image. Pre-upload via `images.create` to get an `image_key` instead. - Using `--content` without making the JSON match the effective `--msg-type`. - Explicitly setting `--msg-type` to something that conflicts with `--text`, `--markdown`, or media flags. - Mixing `--text`, `--markdown`, or `--content` with media flags in one command. @@ -242,4 +242,4 @@ lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Test\n\nhello' --dry - `--as user` uses a user access token (UAT) and requires the `im:message.send_as_user` and `im:message` scopes; the message is sent as the authorized end user - `--as bot` uses a tenant access token (TAT) and requires the `im:message:send_as_bot` scope - When sending as a bot, the app must already be in the target group or already have a direct-message relationship with the target user -- When using `--markdown` with images, all images must be uploaded via `images.create` first to obtain an `image_key`; local paths and remote URLs are not supported as image links directly +- When using `--markdown` with images, pre-uploading via `images.create` to obtain an `image_key` is recommended for reliability; remote URLs may be auto-resolved at runtime, but if download/upload fails the image is removed with a warning; local paths are not supported