diff --git a/shortcuts/im/coverage_additional_test.go b/shortcuts/im/coverage_additional_test.go
index c4389b7c6..f9c931fd8 100644
--- a/shortcuts/im/coverage_additional_test.go
+++ b/shortcuts/im/coverage_additional_test.go
@@ -101,6 +101,9 @@ func TestResolveMarkdownAsPost(t *testing.T) {
if !strings.Contains(got, `#### Title`) || !strings.Contains(got, `##### Subtitle`) {
t.Fatalf("resolveMarkdownAsPost() = %q, want optimized heading levels", got)
}
+ if strings.Contains(got, `
`) {
+ t.Fatalf("resolveMarkdownAsPost() = %q, want no literal
", got)
+ }
}
func TestValidateContentFlags(t *testing.T) {
diff --git a/shortcuts/im/helpers.go b/shortcuts/im/helpers.go
index 15d127733..4c3580573 100644
--- a/shortcuts/im/helpers.go
+++ b/shortcuts/im/helpers.go
@@ -619,31 +619,22 @@ func readMp4Duration(f *os.File, fileSize int64) int64 {
// Steps:
// 1. Extract code blocks with placeholders to protect them
// 2. Downgrade headings: H1 → H4, H2~H6 → H5 (only when H1~H3 present)
-// 3. Add
between consecutive headings
-// 4. Add spacing around tables with
-// 5. Restore code blocks with
wrappers
-// 6. Compress excess blank lines
-// 7. Strip invalid image references (keep only img_xxx keys)
+// 3. Normalize spacing between consecutive headings and tables with blank lines
+// 4. Restore code blocks
+// 5. Compress excess blank lines
+// 6. Strip invalid image references (keep only img_xxx keys)
var (
- reH2toH6 = regexp.MustCompile(`(?m)^#{2,6} (.+)$`)
- reH1 = regexp.MustCompile(`(?m)^# (.+)$`)
- reHasH1toH3 = regexp.MustCompile(`(?m)^#{1,3} `)
- reConsecH = regexp.MustCompile(`(?m)^(#{4,5} .+)\n{1,2}(#{4,5} )`)
- reTableNoGap = regexp.MustCompile(`(?m)^([^|\n].*)\n(\|.+\|)`)
- reTableBefore = regexp.MustCompile(`\n\n((?:\|.+\|[^\S\n]*\n?)+)`)
- reTableAfter = regexp.MustCompile(`(?m)((?:^\|.+\|[^\S\n]*\n?)+)`)
- reTableTxtPre = regexp.MustCompile(`(?m)^([^\n]+)\n\n(
)\n\n(\|)`)
- reTableBoldPre = regexp.MustCompile(`(?m)^(\*\*.+)\n\n(
)\n\n(\|)`)
- reTableTxtPost = regexp.MustCompile(`(?m)(\|[^\n]*\n)\n(
\n)([^\n]+)`)
- reExcessNL = regexp.MustCompile(`\n{3,}`)
- reInvalidImg = regexp.MustCompile(`!\[[^\]]*\]\(([^)\s]+)\)`)
- reCodeBlock = regexp.MustCompile("```[\\s\\S]*?```")
+ reH2toH6 = regexp.MustCompile(`(?m)^#{2,6} (.+)$`)
+ reH1 = regexp.MustCompile(`(?m)^# (.+)$`)
+ reHasH1toH3 = regexp.MustCompile(`(?m)^#{1,3} `)
+ reConsecH = regexp.MustCompile(`(?m)^(#{4,5} .+)\n{1,2}(#{4,5} )`)
+ reTableNoGap = regexp.MustCompile(`(?m)^([^|\n].*)\n(\|.+\|)`)
+ reTableAfter = regexp.MustCompile(`(?m)((?:^\|.+\|[^\S\n]*\n?)+)`)
+ reExcessNL = regexp.MustCompile(`\n{3,}`)
+ reInvalidImg = regexp.MustCompile(`!\[[^\]]*\]\(([^)\s]+)\)`)
+ reCodeBlock = regexp.MustCompile("```[\\s\\S]*?```")
)
-func isTableSpacingProtectedLine(line string) bool {
- return strings.HasPrefix(line, "#### ") || strings.HasPrefix(line, "##### ") || strings.HasPrefix(line, "**")
-}
-
func optimizeMarkdownStyle(text string) string {
const mark = "___CB_"
var codeBlocks []string
@@ -659,29 +650,13 @@ func optimizeMarkdownStyle(text string) string {
r = reH1.ReplaceAllString(r, "#### $1")
}
- r = reConsecH.ReplaceAllString(r, "$1\n
\n$2")
+ r = reConsecH.ReplaceAllString(r, "$1\n\n$2")
r = reTableNoGap.ReplaceAllString(r, "$1\n\n$2")
- r = reTableBefore.ReplaceAllString(r, "\n\n
\n\n$1")
- r = reTableAfter.ReplaceAllString(r, "$1\n
\n")
- r = reTableTxtPre.ReplaceAllStringFunc(r, func(m string) string {
- sub := reTableTxtPre.FindStringSubmatch(m)
- if len(sub) != 4 || isTableSpacingProtectedLine(sub[1]) {
- return m
- }
- return sub[1] + "\n" + sub[2] + "\n" + sub[3]
- })
- r = reTableBoldPre.ReplaceAllString(r, "$1\n$2\n\n$3")
- r = reTableTxtPost.ReplaceAllStringFunc(r, func(m string) string {
- sub := reTableTxtPost.FindStringSubmatch(m)
- if len(sub) != 4 || isTableSpacingProtectedLine(sub[3]) {
- return m
- }
- return sub[1] + sub[2] + sub[3]
- })
+ r = reTableAfter.ReplaceAllString(r, "$1\n")
for i, block := range codeBlocks {
- r = strings.Replace(r, fmt.Sprintf("%s%d___", mark, i), "\n
\n"+block+"\n
\n", 1)
+ r = strings.Replace(r, fmt.Sprintf("%s%d___", mark, i), block, 1)
}
r = reExcessNL.ReplaceAllString(r, "\n\n")
diff --git a/shortcuts/im/helpers_test.go b/shortcuts/im/helpers_test.go
index e5cbf7e0e..7fe18d8a5 100644
--- a/shortcuts/im/helpers_test.go
+++ b/shortcuts/im/helpers_test.go
@@ -282,7 +282,7 @@ func TestOptimizeMarkdownStyle(t *testing.T) {
{
name: "heading downgrade H1 and H2",
input: "# Title\n## Section\ntext",
- want: "#### Title\n
\n##### Section\ntext",
+ want: "#### Title\n\n##### Section\ntext",
},
{
name: "no downgrade when no H1-H3",
@@ -292,17 +292,17 @@ func TestOptimizeMarkdownStyle(t *testing.T) {
{
name: "code block protected",
input: "# Title\n```\n# not a heading\n```\ntext",
- want: "#### Title\n\n
\n```\n# not a heading\n```\n
\n\ntext",
+ want: "#### Title\n```\n# not a heading\n```\ntext",
},
{
name: "table spacing",
input: "text\n| A | B |\n| - | - |\n| 1 | 2 |\nafter",
- want: "text\n
\n| A | B |\n| - | - |\n| 1 | 2 |\n
\nafter",
+ want: "text\n\n| A | B |\n| - | - |\n| 1 | 2 |\n\nafter",
},
{
name: "table spacing keeps heading separation",
input: "# Title\n| A | B |\n| - | - |\n| 1 | 2 |\n## Next",
- want: "#### Title\n\n
\n\n| A | B |\n| - | - |\n| 1 | 2 |\n\n
\n##### Next",
+ want: "#### Title\n\n| A | B |\n| - | - |\n| 1 | 2 |\n\n##### Next",
},
{
name: "excess blank lines compressed",
diff --git a/shortcuts/im/im_messages_send.go b/shortcuts/im/im_messages_send.go
index 986093165..5e9fa2c9a 100644
--- a/shortcuts/im/im_messages_send.go
+++ b/shortcuts/im/im_messages_send.go
@@ -188,7 +188,7 @@ var ImMessagesSend = common.Shortcut{
return output.ErrValidation("%v", err)
}
}
-
+ // Resolve content type
if markdown != "" {
msgType, content = "post", resolveMarkdownAsPost(ctx, runtime, markdown)
} else if mt, c, err := resolveMediaContent(ctx, runtime, text, imageVal, fileVal, videoVal, videoCoverVal, audioVal); err != nil {
diff --git a/skills/lark-im/references/lark-im-messages-reply.md b/skills/lark-im/references/lark-im-messages-reply.md
index 2e92310db..8d4e007ba 100644
--- a/skills/lark-im/references/lark-im-messages-reply.md
+++ b/skills/lark-im/references/lark-im-messages-reply.md
@@ -18,10 +18,88 @@ Replies sent by this tool are visible to other people. Before calling it, you **
When using `--as bot`, the reply is sent in the app's name, so make sure the app has already been added to the target chat.
+## Choose The Right Content Flag
+
+| Need | Recommended flag | Why |
+|------|------|------|
+| Reply with plain text exactly as written | `--text` | Wrapped directly to `{"text":"..."}` |
+| Reply with simple Markdown and accept conversion | `--markdown` | Automatically converted to `post` JSON |
+| Precisely control the reply payload | `--content` | You provide the exact JSON |
+| Reply with media | `--image` / `--file` / `--video` / `--audio` | Shortcut uploads local files automatically |
+
+### `--text` vs `--markdown`
+
+- Use `--text` when the reply should remain plain text and you want exact control over line breaks, spacing, indentation, code samples, or literal Markdown characters.
+- Use `--markdown` when you want a lightweight formatted reply and you accept that the shortcut will normalize and rewrite parts of the content before sending.
+- Use `--content` when you need exact `post` JSON, a card, a title, multiple locales, or any structure that `--markdown` cannot express reliably.
+
+## What `--markdown` Really Does
+
+`--markdown` does **not** send arbitrary raw Markdown to the API.
+
+The shortcut:
+
+1. Forces `msg_type=post`
+2. Resolves remote Markdown images like ``
+3. Normalizes the Markdown for Feishu post rendering
+4. Wraps the final content as:
+
+```json
+{"zh_cn":{"content":[[{"tag":"md","text":"..."}]]}}
+```
+
+So `--markdown` is a convenience mode, not a full Markdown compatibility layer.
+
+### Current Markdown Caveats
+
+- It does **not** promise full CommonMark / GitHub Flavored Markdown support.
+- It always becomes a `post` payload with a single `zh_cn` locale.
+- It does **not** let you set a `post` title.
+- Headings are rewritten:
+ - `# Title` becomes `#### Title`
+ - `##` to `######` are normalized to `#####` when the content contains H1-H3
+- Consecutive headings are separated with blank lines after heading normalization.
+- 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.
+
+If you need exact output, use `--msg-type post --content ...` instead of `--markdown`.
+
+## Preserving Formatting
+
+If the reply contains multiple lines, code blocks, indentation, tabs, or a lot of escaping, prefer `$'...'`.
+
+### When formatting must be preserved
+
+Use `--text` plus `$'...'`:
+
+```bash
+lark-cli im +messages-reply --message-id om_xxx --text $'Received\nI will check this today.\nOwner: alice'
+```
+
+```bash
+lark-cli im +messages-reply --message-id om_xxx --text $'```sql\nselect * from jobs;\n```'
+```
+
+This keeps the reply as plain text instead of converting it to a `post`.
+
+### When formatting does not need exact preservation
+
+Use `--markdown`:
+
+```bash
+lark-cli im +messages-reply --message-id om_xxx --markdown $'## Follow-up\n\n- I reproduced it\n- I am fixing it'
+```
+
+This is better for quick readable formatting, but the final payload may still differ from the source text because headings and spacing are normalized before sending.
+
## Commands
```bash
-# Reply to a message (plain text, bot identity, --text is recommended)
+# Reply to a message (plain text, --text is recommended for normal replies)
lark-cli im +messages-reply --message-id om_xxx --text "Received"
# Equivalent manual JSON
@@ -30,13 +108,16 @@ lark-cli im +messages-reply --message-id om_xxx --content '{"text":"Received"}'
# Reply as a bot
lark-cli im +messages-reply --message-id om_xxx --text "bot reply" --as bot
+# Reply with preserved multi-line text
+lark-cli im +messages-reply --message-id om_xxx --text $'Line 1\nLine 2\n indented line'
+
# Reply inside the thread (message appears in the target thread)
lark-cli im +messages-reply --message-id om_xxx --text "Let's discuss this" --reply-in-thread
-# Bot identity + thread reply
-lark-cli im +messages-reply --message-id om_xxx --text "bot reply" --as bot --reply-in-thread
+# 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 a rich-text message
+# 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"}]]}}'
# Reply with a local image (uploaded automatically before sending)
@@ -52,7 +133,7 @@ lark-cli im +messages-reply --message-id om_xxx --video ./demo.mp4 --video-cover
lark-cli im +messages-reply --message-id om_xxx --text "Received" --idempotency-key my-unique-id
# Preview the request without executing it
-lark-cli im +messages-reply --message-id om_xxx --text "Test" --dry-run
+lark-cli im +messages-reply --message-id om_xxx --markdown $'## Test\n\nhello' --dry-run
```
## Parameters
@@ -60,15 +141,15 @@ lark-cli im +messages-reply --message-id om_xxx --text "Test" --dry-run
| Parameter | Required | Description |
|------|------|------|
| `--message-id ` | Yes | ID of the message being replied to (`om_xxx`) |
-| `--msg-type ` | No | Message type (default `text`): `text`, `post`, `image`, `file`, `audio`, `media`, `interactive`, `share_chat`, `share_user` |
-| `--content ` | One of content options | Reply content as a JSON string; format depends on `msg_type` |
-| `--text ` | One of content options | Plain text message (automatically wrapped as `{"text":"..."}` JSON) |
-| `--markdown ` | One of content options | Markdown text (auto-wrapped as post format with style optimization; image URLs auto-resolved) |
-| `--image ` | One of content options | Local image path, `image_key` (`img_xxx`)|
-| `--file ` | One of content options | Local file path, `file_key` (`file_xxx`)|
-| `--video ` | One of content options | Local video path, `file_key`; **must be used together with `--video-cover`** |
-| `--video-cover ` | **Required with `--video`** | Video cover image path, `image_key` (`img_xxx`) |
-| `--audio ` | One of content options | Local audio path, `file_key` |
+| `--msg-type ` | No | Message type (default `text`). If you use `--text` / `--markdown` / media flags, the effective type is inferred automatically. Explicitly setting a conflicting `--msg-type` fails validation |
+| `--content ` | One content option | Exact reply content as JSON. The JSON must match the effective `--msg-type` |
+| `--text ` | One content option | Plain text reply. Best default when you need exact text and formatting preservation |
+| `--markdown ` | One content option | Convenience Markdown input. Internally converted to `post` JSON with Feishu-specific normalization |
+| `--image ` | One content option | Local image path or `image_key` (`img_xxx`) |
+| `--file ` | One content option | Local file path or `file_key` (`file_xxx`) |
+| `--video ` | One content option | Local video path or `file_key`; **must be used together with `--video-cover`** |
+| `--video-cover ` | **Required with `--video`** | Video cover image path or `image_key` (`img_xxx`) |
+| `--audio ` | One content option | Local audio path or `file_key` |
| `--reply-in-thread` | No | Reply inside the thread. The reply appears in the target message's thread instead of the main chat stream |
| `--idempotency-key ` | No | Idempotency key; the same key sends only one reply within 1 hour |
| `--as ` | No | Identity type: `bot` only |
@@ -78,6 +159,15 @@ lark-cli im +messages-reply --message-id om_xxx --text "Test" --dry-run
>
> **Video cover rule:** `--video` **must** be accompanied by `--video-cover`. Omitting `--video-cover` when using `--video` will fail validation. `--video-cover` cannot be used without `--video`.
+## Common Mistakes
+
+- 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 `--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.
+
## Return Value
```json
@@ -108,16 +198,22 @@ The reply appears in the target message's thread and does not show up in the mai
## @Mention Format (text / post)
-- @specific user: `name`
+- Recommended format: `name`
- @all: ``
+- The shortcut normalizes common variants like `` and `` into `user_id`, but `user_id` remains the recommended documented form
## Notes
- `--message-id` must be a valid message ID in `om_xxx` format
-- `--content` must be a valid JSON string
-- `--reply-in-thread` is only meaningful in group chats
-- `--image`/`--file`/`--video`/`--audio`/`--video-cover` support local file paths; use relative paths within the current working directory. The shortcut automatically uploads the file first and then sends the reply
-- If the provided value starts with `img_` or `file_`, it is treated as an existing key and used directly
-- When using `--video`, `--video-cover` is **required** as the video cover. Omitting `--video-cover` with `--video` will produce a validation error. `--video-cover` cannot be used without `--video`
+- `--content` must be valid JSON
+- When using `--content`, you are responsible for making the JSON structure match the effective `msg_type`
+- `--reply-in-thread` adds `reply_in_thread=true` to the API request
+- `--reply-in-thread` is mainly meaningful in chats that support thread replies
+- `--image`/`--file`/`--video`/`--audio`/`--video-cover` support local file paths; the shortcut uploads first and then sends the reply
+- If the provided media value starts with `img_` or `file_`, it is treated as an existing key and used directly
+- `--markdown` always sends `msg_type=post`
+- If you explicitly set `--msg-type` and it conflicts with the chosen content flag, validation fails
+- When using `--video`, `--video-cover` is required as the video cover
+- `--dry-run` uses placeholder image keys for remote Markdown images and placeholder media keys for local uploads
- Failures return error codes and messages
-- `--as bot` uses a tenant access token (TAT), and requires the `im:message:send_as_bot` scope
+- `--as bot` uses a tenant access token (TAT), and requires the `im:message:send_as_bot` scope
\ No newline at end of file
diff --git a/skills/lark-im/references/lark-im-messages-send.md b/skills/lark-im/references/lark-im-messages-send.md
index 08f752756..c735c75a4 100644
--- a/skills/lark-im/references/lark-im-messages-send.md
+++ b/skills/lark-im/references/lark-im-messages-send.md
@@ -18,10 +18,90 @@ Messages sent by this tool are visible to other people. Before calling it, you *
When using `--as bot`, the message is sent in the app's name, so make sure the app has already been added to the target chat.
+## Choose The Right Content Flag
+
+| Need | Recommended flag | Why |
+|------|------|------|
+| Send plain text exactly as written | `--text` | Wrapped directly to `{"text":"..."}`; no Markdown conversion |
+| Send simple Markdown and accept Feishu-style rendering | `--markdown` | Automatically converted to `post` JSON |
+| Precisely control the final payload | `--content` | You provide the exact JSON for `text` / `post` / `interactive` / `share_*` / media payloads |
+| Send image / file / video / audio | `--image` / `--file` / `--video` / `--audio` | Shortcut uploads local files automatically |
+
+### `--text` vs `--markdown`
+
+- Use `--text` when the content should stay as plain text, including exact line breaks, indentation, code samples, shell snippets, or Markdown characters that should **not** be reinterpreted.
+- Use `--markdown` when you want basic Markdown-style rendering and you accept that the shortcut will normalize and rewrite parts of the content before sending.
+- Use `--content` when `--markdown` is not enough, especially if you need exact `post` JSON, a title, multiple locales, cards, or unsupported rich structures.
+
+## What `--markdown` Really Does
+
+`--markdown` is **not** sent as raw Markdown API content.
+
+The shortcut does all of the following before sending:
+
+1. Forces `msg_type=post`
+2. Resolves remote Markdown images like `` by downloading and uploading them first
+3. Normalizes the Markdown for Feishu post rendering
+4. Wraps the result as:
+
+```json
+{"zh_cn":{"content":[[{"tag":"md","text":"..."}]]}}
+```
+
+This means `--markdown` is convenient, but it is not a full-fidelity Markdown transport.
+
+### Current Markdown Caveats
+
+- It does **not** promise full CommonMark / GitHub Flavored Markdown support.
+- It always becomes a `post` payload with a single `zh_cn` locale.
+- It does **not** let you set a `post` title. If you need a title, use `--msg-type post --content ...`.
+- Headings are rewritten:
+ - `# Title` becomes `#### Title`
+ - `##` to `######` are normalized to `#####` when the content contains H1-H3
+- Consecutive headings are separated with blank lines after heading normalization.
+- 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.
+
+If any of the above is unacceptable, do **not** use `--markdown`; use `--content` and provide the final JSON yourself.
+
+## Preserving Formatting
+
+If the message has multiple lines, indentation, code blocks, tabs, or many quotes/backslashes, prefer shell ANSI-C quoting with `$'...'`.
+
+This is especially useful in `zsh` / `bash` because it lets you write `\n` explicitly instead of relying on the shell to preserve literal newlines.
+
+### When formatting must be preserved
+
+Use `--text` plus `$'...'`:
+
+```bash
+lark-cli im +messages-send --chat-id oc_xxx --text $'Build failed\nBranch: feature/im-docs\nAction: please check logs'
+```
+
+```bash
+lark-cli im +messages-send --chat-id oc_xxx --text $'```bash\nmake test\nmake lint\n```'
+```
+
+Use this path when you want the receiver to see the text exactly as entered, not a converted Markdown post.
+
+### When formatting does not need exact preservation
+
+Use `--markdown`:
+
+```bash
+lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Release Notes\n\n- Added send shortcut\n- Added reply shortcut'
+```
+
+This is better for lightweight readable formatting, but the final content may not match the source text byte-for-byte because the shortcut normalizes headings and spacing before sending.
+
## Commands
```bash
-# Send plain text (--text is recommended; it is wrapped into JSON automatically)
+# Send plain text (--text is recommended for normal messages)
lark-cli im +messages-send --chat-id oc_xxx --text "Hello"
# Equivalent manual JSON
@@ -30,7 +110,13 @@ lark-cli im +messages-send --chat-id oc_xxx --content '{"text":"Hello"}'
# Send to a direct message (pass open_id)
lark-cli im +messages-send --user-id ou_xxx --text "Hello"
-# Send a rich-text message
+# Send multi-line text while preserving formatting
+lark-cli im +messages-send --chat-id oc_xxx --text $'Line 1\nLine 2\n indented line'
+
+# 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'
+
+# 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"}]]}}'
# Send a local image (uploaded automatically before sending)
@@ -53,7 +139,7 @@ lark-cli im +messages-send --chat-id oc_xxx --audio ./voice.opus
lark-cli im +messages-send --chat-id oc_xxx --text "Hello" --idempotency-key my-unique-id
# Preview the request without executing it
-lark-cli im +messages-send --chat-id oc_xxx --text "Test" --dry-run
+lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Test\n\nhello' --dry-run
```
## Parameters
@@ -62,15 +148,15 @@ lark-cli im +messages-send --chat-id oc_xxx --text "Test" --dry-run
|------|------|------|
| `--chat-id ` | One of two | Group chat ID (`oc_xxx`) |
| `--user-id ` | One of two | User open_id (`ou_xxx`) for direct messages |
-| `--text ` | One of seven content options | Plain text message (automatically wrapped as `{"text":"..."}` JSON) |
-| `--markdown ` | One of seven content options | Markdown text (auto-wrapped as post format with style optimization; image URLs auto-resolved) |
-| `--content ` | One of seven content options | Message content JSON string; format depends on `msg_type` |
-| `--image ` | One of seven content options | Local image path or `image_key` (`img_xxx`). Local paths are uploaded automatically |
-| `--file ` | One of seven content options | Local file path or `file_key` (`file_xxx`). Local paths are uploaded automatically |
-| `--video ` | One of seven content options | Local video path or `file_key`. Local paths are uploaded automatically. **Must be paired with `--video-cover`** |
+| `--text ` | One content option | Plain text message. Best default for exact text and preserved formatting. Automatically wrapped as `{"text":"..."}` |
+| `--markdown ` | One content option | Convenience Markdown input. Internally converted to `post` JSON with Feishu-specific normalization; not full Markdown passthrough |
+| `--content ` | One content option | Exact message content JSON string; use this when you need full control over `msg_type` and payload. The JSON must match the effective `--msg-type` |
+| `--image ` | One content option | Local image path or `image_key` (`img_xxx`). Local paths are uploaded automatically |
+| `--file ` | One content option | Local file path or `file_key` (`file_xxx`). Local paths are uploaded automatically |
+| `--video ` | One content option | Local video path or `file_key`. Local paths are uploaded automatically. **Must be paired with `--video-cover`** |
| `--video-cover ` | **Required with `--video`** | Video cover image path or `image_key` (`img_xxx`). Local paths are uploaded automatically |
-| `--audio ` | One of seven content options | Local audio path or `file_key`. Local paths are uploaded automatically |
-| `--msg-type ` | No | Message type (default `text`): `text`, `post`, `image`, `file`, `audio`, `media`, `interactive`, `share_chat`, `share_user`. Automatically set when using `--text`/`--image`/`--file`/`--video`/`--audio` |
+| `--audio ` | One content option | Local audio path or `file_key`. Local paths are uploaded automatically |
+| `--msg-type ` | No | Message type (default `text`). If you use `--text` / `--markdown` / media flags, the effective type is inferred automatically. Explicitly setting a conflicting `--msg-type` fails validation |
| `--idempotency-key ` | No | Idempotency key; the same key sends only one message within 1 hour |
| `--as ` | No | Identity type: `bot` only |
| `--dry-run` | No | Print the request only, do not execute it |
@@ -79,6 +165,15 @@ lark-cli im +messages-send --chat-id oc_xxx --text "Test" --dry-run
>
> **Video cover rule:** `--video` **must** be accompanied by `--video-cover`. Omitting `--video-cover` when using `--video` will fail validation. `--video-cover` cannot be used without `--video`.
+## Common Mistakes
+
+- 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 `--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.
+
## `content` Format Reference
| `msg_type` | Example `content` |
@@ -105,16 +200,21 @@ lark-cli im +messages-send --chat-id oc_xxx --text "Test" --dry-run
## @Mention Format (text / post)
-- @specific user: `name`
+- Recommended format: `name`
- @all: ``
+- The shortcut normalizes common variants like `` and `` into `user_id`, but you should still document examples with `user_id`
## Notes
- `--chat-id` and `--user-id` are mutually exclusive; you must provide exactly one
-- `--content` must be a valid JSON string
-- `--image`/`--file`/`--video`/`--audio` support local file paths; use relative paths within the current working directory. The shortcut automatically uploads the file first and then sends the message. You do not need to call a separate upload command manually
-- If the provided value starts with `img_` or `file_`, it is treated as an existing key and used directly
-- When using `--video`, `--video-cover` is **required** as the video cover (`image_key`). Omitting `--video-cover` with `--video` will produce a validation error. `--video-cover` cannot be used without `--video`
+- `--content` must be valid JSON
+- When using `--content`, you are responsible for making the JSON structure match the effective `msg_type`
+- `--image`/`--file`/`--video`/`--audio` support local file paths; the shortcut uploads first and then sends the message
+- If the provided media value starts with `img_` or `file_`, it is treated as an existing key and used directly
+- `--markdown` always sends `msg_type=post`, even if you do not explicitly set `--msg-type post`
+- If you explicitly set `--msg-type` and it conflicts with the chosen content flag, validation fails
+- When using `--video`, `--video-cover` is required as the video cover
+- `--dry-run` uses placeholder image keys for remote Markdown images and placeholder media keys for local uploads
- Failures return an error code and message
- `--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 sending as a bot, the app must already be in the target group or already have a direct-message relationship with the target user
\ No newline at end of file