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 `![x](https://...)` +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 `![x](./a.png)` 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 `![x](./a.png)`. `--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 `![x](https://...)` 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 `![x](./a.png)` 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 `![x](./a.png)`. `--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