mirror of
https://github.com/larksuite/cli.git
synced 2026-07-08 18:13:01 +08:00
feat(im): support Markdown image rendering in post content (#893)
add documentation for sending Markdown images, and align image handling guidance with actual runtime behavior
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -373,171 +373,19 @@ 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: "<tag>", want: `"<tag>"`},
|
||||
{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"`},
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
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**")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldUseSegmentedPost(t *testing.T) {
|
||||
|
||||
@@ -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 `` are **not** auto-uploaded by `--markdown`.
|
||||
- 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. ``) 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` with images, prefer pre-uploading via `images.create` and referencing `` for predictable results. Remote URLs may work but are not guaranteed.
|
||||
|
||||
**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\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\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"}]]}}'
|
||||
|
||||
@@ -172,6 +192,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 ``. `--markdown` does not auto-upload those paths.
|
||||
- **Using local file paths inside Markdown image syntax** (e.g. ``) 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.
|
||||
@@ -226,3 +247,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, 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
|
||||
|
||||
@@ -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 `` are **not** auto-uploaded by `--markdown`; they may be stripped during optimization.
|
||||
- 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 `` 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` with images, prefer pre-uploading via `images.create` and referencing `` for predictable results. Remote URLs may work but are not guaranteed.
|
||||
|
||||
**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\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\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"}]]}}'
|
||||
|
||||
@@ -178,6 +198,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 ``. `--markdown` does not auto-upload those paths.
|
||||
- **Using local file paths inside Markdown image syntax** (e.g. ``) 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.
|
||||
@@ -227,3 +248,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, 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
|
||||
|
||||
Reference in New Issue
Block a user