- 移除 TodoItem 中的 priority、created_at 和 updated_at 字段 - 强制每个任务都必须有唯一 id,且由用户负责生成 - 修改合并模式逻辑,merge=true 下保留未提及的旧任务 - 支持已完成和已取消任务重新激活(状态改回 pending 或 in_progress) - 禁止 in_progress 状态退回到 pending,必须标记为 completed 或 cancelled - 优化状态转换校验,允许特定状态间合法切换 - 简化任务变更消息,移除详细的新增/更新/移除统计 - 更新文档和示例,明确 id 必须由用户生成和使用 - 修复和补充测试,增强状态转换和合并模式验证 - 调整任务时间戳生成逻辑,统一使用当前时间及索引 - 该变更提供更合理的任务状态机械及管理模式,提升稳定性和易用性
15 KiB
im +messages-send
Prerequisite: Read
../lark-shared/SKILL.mdfirst to understand authentication, global parameters, and safety rules.
Send a message to a group chat or a direct message conversation. Supports both user identity (--as user) and bot identity (--as bot).
This skill maps to the shortcut: lark-cli im +messages-send (internally calls POST /open-apis/im/v1/messages).
Safety Constraints
Messages sent by this tool are visible to other people. Before calling it, you must confirm with the user:
- The recipient (which person or which group)
- The message content
- The sending identity (user or bot)
Do not send messages without explicit user approval.
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.
When using --as user, the message is sent as the authorized end user and requires the im:message.send_as_user and im:message scopes.
Choose The Right Content Flag
Default Selection Rule For Agents
- Prefer
--markdownfor headings, lists, links, summaries, reports, or Markdown-looking content. - Use
--textfor exact plain text: logs, code, indentation-sensitive text, or literal Markdown. - Use
--contentfor exactpostJSON, titles, multiple locales, cards, or unsupported structures.
| Need | Recommended flag | Why |
|---|---|---|
| Send headings, lists, links, summaries, or reports | --markdown |
Best default for lightweight formatting; converted to Feishu post JSON |
| Send plain text exactly as written | --text |
Preserves literal text; no Markdown conversion |
| 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 URLs, or cwd-relative local files automatically |
--text vs --markdown
- Use
--markdownfor lightweight formatted messages. - Use
--textfor exact plain text, especially logs, code, indentation, or Markdown characters that should not render. - Use
--contentwhen--markdownis not enough, especially if you need exactpostJSON, a title, multiple locales, cards, or unsupported rich structures.
What --markdown Really Does
--markdown accepts Markdown-like input and converts it to the Feishu post payload required by the message API.
The shortcut does all of the following before sending:
- Forces
msg_type=post - Resolves remote Markdown images like
by downloading and uploading them first - Normalizes the Markdown for Feishu post rendering
- Wraps the result as:
{"zh_cn":{"content":[[{"tag":"md","text":"..."}]]}}
This makes --markdown the simplest path for lightweight formatted messages.
Markdown Boundaries
- It does not promise full CommonMark / GitHub Flavored Markdown support.
- It always becomes a
postpayload with a singlezh_cnlocale. - It does not let you set a
posttitle. If you need a title, use--msg-type post --content .... - Headings are rewritten:
# Titlebecomes#### 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.
- Already-uploaded
img_xxximage 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 you need a title, multiple locales, cards, unsupported rich structures, or byte-for-byte post JSON control, 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:
# 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 $'...' for either --markdown or --text.
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 $'...':
lark-cli im +messages-send --chat-id oc_xxx --text $'Build failed\nBranch: feature/im-docs\nAction: please check logs'
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.
Commands
# Send a formatted update
lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Update\n\n- item 1\n- item 2'
# Send a plain one-line message
lark-cli im +messages-send --chat-id oc_xxx --text "Hello"
# Equivalent manual JSON
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 multi-line text while preserving formatting
lark-cli im +messages-send --chat-id oc_xxx --text $'Line 1\nLine 2\n indented line'
# 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"}]]}}'
# Send a local image (uploaded automatically before sending)
lark-cli im +messages-send --chat-id oc_xxx --image ./photo.png
# Or send directly with an existing image_key
lark-cli im +messages-send --chat-id oc_xxx --image img_xxx
# Send a local file (uploaded automatically before sending)
lark-cli im +messages-send --chat-id oc_xxx --file ./report.pdf
# Send a video (--video-cover is required as the cover)
lark-cli im +messages-send --chat-id oc_xxx --video ./demo.mp4 --video-cover ./cover.png
lark-cli im +messages-send --chat-id oc_xxx --video ./demo.mp4 --video-cover img_xxx
# Send audio
lark-cli im +messages-send --chat-id oc_xxx --audio ./voice.opus
# Use an idempotency key (same key sends only once within 1 hour)
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 --markdown $'## Test\n\nhello' --dry-run
Media Input Rules
- Media flags accept an existing key (
img_xxx/file_xxx), anhttp://orhttps://URL, or a local file path. - Local paths must be relative to the current working directory and stay within it after resolving
..and symlinks. - Absolute paths such as
/tmp/photo.pngare rejected. Run the command from the file's directory and pass./photo.png, or copy the file into the current directory first.
Parameters
| Parameter | Required | Description |
|---|---|---|
--chat-id <id> |
One of two | Group chat ID (oc_xxx) |
--user-id <id> |
One of two | User open_id (ou_xxx) for direct messages |
--text <string> |
One content option | Plain text message. Use when exact text and formatting preservation matter. Automatically wrapped as {"text":"..."} |
--markdown <string> |
One content option | Best default for lightweight formatted messages such as headings, lists, links, summaries, and reports. Internally converted to post JSON with Feishu-specific normalization |
--content <json> |
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 <path|url|key> |
One content option | Cwd-relative local image path, URL, or image_key (img_xxx). Local paths and URLs are uploaded automatically |
--file <path|url|key> |
One content option | Cwd-relative local file path, URL, or file_key (file_xxx). Local paths and URLs are uploaded automatically |
--video <path|url|key> |
One content option | Cwd-relative local video path, URL, or file_key (file_xxx). Local paths and URLs are uploaded automatically. Must be paired with --video-cover |
--video-cover <path|url|key> |
Required with --video |
Cwd-relative local cover image path, URL, or image_key (img_xxx). Local paths and URLs are uploaded automatically |
--audio <path|url|key> |
One content option | Cwd-relative local audio path, URL, or file_key (file_xxx). Local paths and URLs are uploaded automatically |
--msg-type <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 <key> |
No | Idempotency key; the same key sends only one message within 1 hour |
--as <identity> |
No | Identity type: bot or user (default bot) |
--dry-run |
No | Print the request only, do not execute it |
Mutual exclusivity rule:
--text,--markdown,--content, and--image/--file/--video/--audiocannot be used together. Media flags are also mutually exclusive with each other.Video cover rule:
--videomust be accompanied by--video-cover. Omitting--video-coverwhen using--videowill fail validation.--video-covercannot be used without--video.
Common Mistakes
- Choosing
--textfor headings, lists, links, summaries, or reports. Use--markdown. - Choosing
--markdownwhen you actually need exact plain text. If exact line breaks, spacing, logs, code, or literal Markdown characters matter, use--text, usually with$'...'. - Assuming
--markdownsupports every Markdown feature. It is converted into a Feishupostpayload and normalized first. - Putting local image paths inside Markdown like
.--markdowndoes 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 viaimages.createto get animage_keyinstead. - Using
--contentwithout making the JSON match the effective--msg-type. - Explicitly setting
--msg-typeto something that conflicts with--text,--markdown, or media flags. - Mixing
--text,--markdown, or--contentwith media flags in one command.
content Format Reference
msg_type |
Example content |
|---|---|
text |
{"text":"Hello <at user_id=\"ou_xxx\">name</at>"} |
post |
{"zh_cn":{"title":"Title","content":[[{"tag":"text","text":"Body"}]]}} |
image |
{"image_key":"img_xxx"} |
file |
{"file_key":"file_xxx"} |
audio |
{"file_key":"file_xxx"} |
media |
{"file_key":"file_xxx","image_key":"img_xxx"} (video; image_key is the cover from --video-cover — required) |
share_chat |
{"chat_id":"oc_xxx"} |
share_user |
{"user_id":"ou_xxx"} |
interactive |
Card JSON (see Feishu interactive card documentation) |
Return Value
{
"message_id": "om_xxx",
"chat_id": "oc_xxx",
"create_time": "1234567890"
}
@Mention Format (text / post)
- Recommended format:
<at user_id="ou_xxx">name</at> - @all:
<at user_id="all"></at> - The shortcut normalizes common variants like
<at id=...>and<at open_id=...>intouser_id, but you should still document examples withuser_id
Notes
--chat-idand--user-idare mutually exclusive; you must provide exactly one--contentmust be valid JSON- When using
--content, you are responsible for making the JSON structure match the effectivemsg_type --image/--file/--video/--audiosupport existing keys, URLs, and cwd-relative local file paths; the shortcut uploads local paths and URLs first, then sends the message; both the upload and send steps use the same identity (UAT when--as user, TAT when--as bot)- If the provided media value starts with
img_orfile_, it is treated as an existing key and used directly --markdownalways sendsmsg_type=post, even if you do not explicitly set--msg-type post- If you explicitly set
--msg-typeand it conflicts with the chosen content flag, validation fails - When using
--video,--video-coveris required as the video cover --dry-runuses placeholder image keys for remote Markdown images and placeholder media keys for local uploads- Failures return an error code and message
--as useruses a user access token (UAT) and requires theim:message.send_as_userandim:messagescopes; the message is sent as the authorized end user--as botuses a tenant access token (TAT) and requires theim:message:send_as_botscope- 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
--markdownwith images, pre-uploading viaimages.createto obtain animage_keyis 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