Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions shortcuts/im/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,24 @@ func TestShortcuts(t *testing.T) {
}
}

func TestMessageBodyFlagsAcceptFileAndStdin(t *testing.T) {
for _, shortcut := range []common.Shortcut{ImMessagesSend, ImMessagesReply} {
for _, name := range []string{"content", "text", "markdown"} {
var got []string
for _, flag := range shortcut.Flags {
if flag.Name == name {
got = flag.Input
break
}
}
want := []string{common.File, common.Stdin}
if !reflect.DeepEqual(got, want) {
t.Fatalf("%s --%s Input = %#v, want %#v", shortcut.Command, name, got, want)
}
}
}
}

// TestSenderDisplay covers the human-readable sender column: a resolved name wins,
// otherwise the sender id is shown (AC3 fallback), and a system/senderless message
// with neither yields an empty string (no name is normal, not an error).
Expand Down
6 changes: 3 additions & 3 deletions shortcuts/im/im_messages_reply.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ var ImMessagesReply = common.Shortcut{
Flags: []common.Flag{
{Name: "message-id", Desc: "message ID (om_xxx)", Required: true},
{Name: "msg-type", Default: "text", Desc: "message type for --content JSON; when using --text/--markdown/--image/--file/--video/--audio, the effective type is inferred automatically", Enum: []string{"text", "post", "image", "file", "audio", "media", "interactive", "share_chat", "share_user"}},
{Name: "content", Desc: "(one of --content/--text/--markdown/--image/--file/--video/--audio required) message content JSON"},
{Name: "text", Desc: "plain text message (auto-wrapped as JSON)"},
{Name: "markdown", Desc: "markdown text (auto-wrapped as post format with style optimization; image URLs auto-resolved)"},
{Name: "content", Desc: "(one of --content/--text/--markdown/--image/--file/--video/--audio required) message content JSON", Input: []string{common.File, common.Stdin}},
{Name: "text", Desc: "plain text message (auto-wrapped as JSON)", Input: []string{common.File, common.Stdin}},
{Name: "markdown", Desc: "markdown text (auto-wrapped as post format with style optimization; image URLs auto-resolved)", Input: []string{common.File, common.Stdin}},
{Name: "image", Desc: "image key (img_xxx), URL, or cwd-relative local path (absolute paths and .. are rejected)"},
{Name: "file", Desc: "file key (file_xxx), URL, or cwd-relative local path (absolute paths and .. are rejected)"},
{Name: "video", Desc: "video file key (file_xxx), URL, or cwd-relative local path (absolute paths and .. are rejected); must be used together with --video-cover"},
Expand Down
6 changes: 3 additions & 3 deletions shortcuts/im/im_messages_send.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ var ImMessagesSend = common.Shortcut{
{Name: "chat-id", Desc: "(required, mutually exclusive with --user-id) chat ID (oc_xxx)"},
{Name: "user-id", Desc: "(required, mutually exclusive with --chat-id) user open_id (ou_xxx)"},
{Name: "msg-type", Default: "text", Desc: "message type for --content JSON; when using --text/--markdown/--image/--file/--video/--audio, the effective type is inferred automatically", Enum: []string{"text", "post", "image", "file", "audio", "media", "interactive", "share_chat", "share_user"}},
{Name: "content", Desc: "(one of --content/--text/--markdown/--image/--file/--video/--audio required) message content JSON"},
{Name: "text", Desc: "plain text message (auto-wrapped as JSON)"},
{Name: "markdown", Desc: "markdown text (auto-wrapped as post format with style optimization; image URLs auto-resolved)"},
{Name: "content", Desc: "(one of --content/--text/--markdown/--image/--file/--video/--audio required) message content JSON", Input: []string{common.File, common.Stdin}},
{Name: "text", Desc: "plain text message (auto-wrapped as JSON)", Input: []string{common.File, common.Stdin}},
{Name: "markdown", Desc: "markdown text (auto-wrapped as post format with style optimization; image URLs auto-resolved)", Input: []string{common.File, common.Stdin}},
{Name: "idempotency-key", Desc: "idempotency key, max 50 characters (prevents duplicate sends)"},
{Name: "image", Desc: "image key (img_xxx), URL, or cwd-relative local path (absolute paths and .. are rejected)"},
{Name: "file", Desc: "file key (file_xxx), URL, or cwd-relative local path (absolute paths and .. are rejected)"},
Expand Down
2 changes: 2 additions & 0 deletions skills/lark-im/references/lark-im-messages-reply.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ When using `--as user`, the reply is sent as the authorized end user and require
- Use `--text` for exact plain text, especially logs, code, indentation, or literal Markdown characters.
- Use `--content` when you need exact `post` JSON, a card, a title, multiple locales, or any structure that `--markdown` cannot express reliably.

For long or generated replies, pass `--markdown @./reply.md` or pipe the body with `--markdown -` so shell metacharacters are never interpreted.

## What `--markdown` Really Does

`--markdown` accepts Markdown-like input and converts it to the Feishu `post` payload required by the reply API.
Expand Down
6 changes: 6 additions & 0 deletions skills/lark-im/references/lark-im-messages-send.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ 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'

# Safest option for long or generated content: read a cwd-relative file
lark-cli im +messages-send --chat-id oc_xxx --markdown @./message.md

# Or pipe exactly one body flag through stdin
generate-message | lark-cli im +messages-send --chat-id oc_xxx --markdown -

# 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
Expand Down
47 changes: 47 additions & 0 deletions tests/cli_e2e/im/message_markdown_input_dryrun_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package im

import (
"context"
"os"
"path/filepath"
"testing"
"time"

clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)

func TestIMMessagesSendMarkdownFileDryRunPreservesShellSyntax(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
t.Setenv("LARKSUITE_CLI_APP_ID", "im_markdown_input_test")
t.Setenv("LARKSUITE_CLI_APP_SECRET", "im_markdown_input_secret")
t.Setenv("LARKSUITE_CLI_BRAND", "feishu")

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

workDir := t.TempDir()
markdown := "## 发布清单\n\n```sh\necho $HOME && $(do-not-run)\n```\n\n中文内容保持完整。\n"
require.NoError(t, os.WriteFile(filepath.Join(workDir, "message.md"), []byte(markdown), 0o600))

result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"im", "+messages-send",
"--chat-id", "oc_123",
"--markdown", "@./message.md",
"--dry-run",
},
DefaultAs: "bot",
WorkDir: workDir,
})
require.NoError(t, err)
result.AssertExitCode(t, 0)

content := gjson.Get(result.Stdout, "data.request.body.content").String()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find existing dry-run gjson.Get calls to confirm the expected path structure.
rg -n 'gjson\.Get.*body' tests/cli_e2e/ -C 1

Repository: larksuite/cli

Length of output: 8574


🏁 Script executed:

#!/bin/bash
sed -n '1,140p' tests/cli_e2e/im/message_markdown_input_dryrun_test.go
printf '\n---\n'
rg -n 'DryRunData|request\.body|data\.request|body\.content|projection\.body|api\.[0-9]+\.body' tests/cli_e2e -S

Repository: larksuite/cli

Length of output: 31785


🏁 Script executed:

#!/bin/bash
rg -n '"data\.request\.body\.content"|data\.request\.body|request\.body\.content|DryRunGet\(|DryRunData\(' . -S

Repository: larksuite/cli

Length of output: 50369


🏁 Script executed:

#!/bin/bash
sed -n '1,140p' tests/cli_e2e/core.go
printf '\n---\n'
rg -n 'dry[- ]run|request\.body|body\.content|gjson|Stdout|DryRun' im tests/cli_e2e/im -S

Repository: larksuite/cli

Length of output: 24933


🏁 Script executed:

#!/bin/bash
rg -n 'messages-send|--markdown|request\.body\.content|dry[- ]run.*request|request\.body' . -S --glob '!tests/cli_e2e/im/message_markdown_input_dryrun_test.go'

Repository: larksuite/cli

Length of output: 46524


🏁 Script executed:

#!/bin/bash
sed -n '1,180p' shortcuts/im/im_messages_send.go
printf '\n---\n'
sed -n '900,1045p' shortcuts/im/helpers.go
printf '\n---\n'
sed -n '240,340p' internal/cmdutil/dryrun.go

Repository: larksuite/cli

Length of output: 15184


🏁 Script executed:

#!/bin/bash
rg -n 'type DryRunAPI|func \(.*DryRunAPI.*Format|func NewDryRunAPI|type dryRun|WriteSuccessEnvelope|SuccessEnvelope' internal shortcuts -S

Repository: larksuite/cli

Length of output: 4623


🏁 Script executed:

#!/bin/bash
sed -n '1,240p' internal/cmdutil/dryrun.go
printf '\n---\n'
sed -n '1,120p' shortcuts/apps/dryrun_test.go
printf '\n---\n'
sed -n '1,220p' internal/output/envelope_success.go

Repository: larksuite/cli

Length of output: 8992


Read the body from data.api.0.body.content. The dry-run envelope stores request previews under data.api[...], so data.request.body.content is empty here. Use clie2e.DryRunGet(result.Stdout, "api.0.body.content") instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/cli_e2e/im/message_markdown_input_dryrun_test.go` at line 44, Update
the content extraction in the dry-run test to read the request preview from the
`data.api` envelope by using `clie2e.DryRunGet(result.Stdout,
"api.0.body.content")` instead of querying `data.request.body.content` directly.

require.Contains(t, content, "echo $HOME && $(do-not-run)")
require.Contains(t, content, "中文内容保持完整。")
}
Loading