From df065ce4b1e0de2c599a9a3cef5a2cac2874d4c0 Mon Sep 17 00:00:00 2001 From: "maojinze.7" Date: Wed, 29 Apr 2026 16:52:48 +0800 Subject: [PATCH 01/10] feat(feed): scaffold feed domain and +sensitive stub --- shortcuts/feed/feed_sensitive.go | 30 ++++++++++++++++++++++++++++++ shortcuts/feed/shortcuts.go | 13 +++++++++++++ shortcuts/register.go | 2 ++ 3 files changed, 45 insertions(+) create mode 100644 shortcuts/feed/feed_sensitive.go create mode 100644 shortcuts/feed/shortcuts.go diff --git a/shortcuts/feed/feed_sensitive.go b/shortcuts/feed/feed_sensitive.go new file mode 100644 index 000000000..44509e08d --- /dev/null +++ b/shortcuts/feed/feed_sensitive.go @@ -0,0 +1,30 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package feed + +import ( + "context" + + "github.com/larksuite/cli/shortcuts/common" +) + +var FeedSensitive = common.Shortcut{ + Service: "feed", + Command: "+sensitive", + Description: "Set or unset time-sensitive (即时提醒) status for a feed card (group chat) for specified users; bot only", + Risk: "write", + BotScopes: []string{"im:datasync.feed_card.time_sensitive:write"}, + AuthTypes: []string{"bot"}, + HasFormat: true, + Flags: []common.Flag{ + {Name: "feed-card-id", Desc: "feed card ID (oc_xxx); group chats only", Required: true}, + {Name: "enable", Type: "bool", Desc: "enable time-sensitive (pin card to top for specified users)"}, + {Name: "disable", Type: "bool", Desc: "disable time-sensitive"}, + {Name: "user-ids", Type: "string_slice", Desc: "user ID list (comma-separated or repeatable); must be members of the feed card chat", Required: true}, + {Name: "user-id-type", Default: "open_id", Desc: "user ID type", Enum: []string{"open_id", "union_id", "user_id"}}, + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + return nil // TODO + }, +} diff --git a/shortcuts/feed/shortcuts.go b/shortcuts/feed/shortcuts.go new file mode 100644 index 000000000..062420313 --- /dev/null +++ b/shortcuts/feed/shortcuts.go @@ -0,0 +1,13 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package feed + +import "github.com/larksuite/cli/shortcuts/common" + +// Shortcuts returns all feed shortcuts. +func Shortcuts() []common.Shortcut { + return []common.Shortcut{ + FeedSensitive, + } +} diff --git a/shortcuts/register.go b/shortcuts/register.go index 534163ec2..a3a178ca4 100644 --- a/shortcuts/register.go +++ b/shortcuts/register.go @@ -6,6 +6,7 @@ package shortcuts import ( "context" + "github.com/larksuite/cli/shortcuts/feed" "github.com/larksuite/cli/shortcuts/okr" "github.com/spf13/cobra" @@ -49,6 +50,7 @@ func init() { allShortcuts = append(allShortcuts, whiteboard.Shortcuts()...) allShortcuts = append(allShortcuts, wiki.Shortcuts()...) allShortcuts = append(allShortcuts, okr.Shortcuts()...) + allShortcuts = append(allShortcuts, feed.Shortcuts()...) } // AllShortcuts returns a copy of all registered shortcuts (for dump-shortcuts). From 01d222a381c687c788e60c8372a29988f3f9922f Mon Sep 17 00:00:00 2001 From: "maojinze.7" Date: Wed, 29 Apr 2026 16:57:27 +0800 Subject: [PATCH 02/10] test(feed): add feed package registration smoke test; fix Flag.Type doc --- shortcuts/common/types.go | 2 +- shortcuts/feed/shortcuts_test.go | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 shortcuts/feed/shortcuts_test.go diff --git a/shortcuts/common/types.go b/shortcuts/common/types.go index 69c597e9d..5b477946c 100644 --- a/shortcuts/common/types.go +++ b/shortcuts/common/types.go @@ -18,7 +18,7 @@ const ( // Flag describes a CLI flag for a shortcut. type Flag struct { Name string // flag name (e.g. "calendar-id") - Type string // "string" (default) | "bool" | "int" | "string_array" + Type string // "string" (default) | "bool" | "int" | "string_array" | "string_slice" Default string // default value as string Desc string // help text Hidden bool // hidden from --help, still readable at runtime diff --git a/shortcuts/feed/shortcuts_test.go b/shortcuts/feed/shortcuts_test.go new file mode 100644 index 000000000..8b3ef6862 --- /dev/null +++ b/shortcuts/feed/shortcuts_test.go @@ -0,0 +1,20 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package feed + +import "testing" + +func TestShortcuts_Registration(t *testing.T) { + shortcuts := Shortcuts() + if len(shortcuts) != 1 { + t.Fatalf("expected 1 shortcut, got %d", len(shortcuts)) + } + s := shortcuts[0] + if s.Service != "feed" { + t.Errorf("expected Service=%q, got %q", "feed", s.Service) + } + if s.Command != "+sensitive" { + t.Errorf("expected Command=%q, got %q", "+sensitive", s.Command) + } +} From d2b8c37915188f03ae4af54e0836a635ef2a33ab Mon Sep 17 00:00:00 2001 From: "maojinze.7" Date: Wed, 29 Apr 2026 16:59:40 +0800 Subject: [PATCH 03/10] feat(feed): add +sensitive validation and dry-run --- shortcuts/feed/feed_sensitive.go | 44 +++++++++++- shortcuts/feed/feed_sensitive_test.go | 98 +++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 shortcuts/feed/feed_sensitive_test.go diff --git a/shortcuts/feed/feed_sensitive.go b/shortcuts/feed/feed_sensitive.go index 44509e08d..d75629d1c 100644 --- a/shortcuts/feed/feed_sensitive.go +++ b/shortcuts/feed/feed_sensitive.go @@ -5,8 +5,13 @@ package feed import ( "context" + "fmt" + "net/http" + "github.com/larksuite/cli/internal/output" + "github.com/larksuite/cli/internal/validate" "github.com/larksuite/cli/shortcuts/common" + larkcore "github.com/larksuite/oapi-sdk-go/v3/core" ) var FeedSensitive = common.Shortcut{ @@ -24,7 +29,44 @@ var FeedSensitive = common.Shortcut{ {Name: "user-ids", Type: "string_slice", Desc: "user ID list (comma-separated or repeatable); must be members of the feed card chat", Required: true}, {Name: "user-id-type", Default: "open_id", Desc: "user ID type", Enum: []string{"open_id", "union_id", "user_id"}}, }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + feedCardID := runtime.Str("feed-card-id") + userIDs := runtime.StrSlice("user-ids") + userIDType := runtime.Str("user-id-type") + timeSensitive := runtime.Changed("enable") + return common.NewDryRunAPI(). + PATCH(fmt.Sprintf("/open-apis/im/v2/feed_cards/%s", validate.EncodePathSegment(feedCardID))). + Params(map[string]interface{}{"user_id_type": userIDType}). + Body(map[string]interface{}{ + "time_sensitive": timeSensitive, + "user_ids": userIDs, + }) + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + feedCardID := runtime.Str("feed-card-id") + if _, err := common.ValidateChatID(feedCardID); err != nil { + return err + } + + enableChanged := runtime.Changed("enable") + disableChanged := runtime.Changed("disable") + if enableChanged && disableChanged { + return common.FlagErrorf("--enable and --disable are mutually exclusive") + } + if !enableChanged && !disableChanged { + return common.FlagErrorf("specify exactly one of --enable or --disable") + } + + userIDs := runtime.StrSlice("user-ids") + if len(userIDs) == 0 { + return output.ErrValidation("--user-ids is required and must not be empty") + } + + return nil + }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { - return nil // TODO + _ = larkcore.QueryParams{} + _ = http.MethodPatch + return nil // TODO: implement in Task 3 }, } diff --git a/shortcuts/feed/feed_sensitive_test.go b/shortcuts/feed/feed_sensitive_test.go new file mode 100644 index 000000000..1c0f252be --- /dev/null +++ b/shortcuts/feed/feed_sensitive_test.go @@ -0,0 +1,98 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package feed + +import ( + "bytes" + "context" + "strings" + "testing" + + "github.com/larksuite/cli/internal/cmdutil" + "github.com/larksuite/cli/internal/core" + "github.com/larksuite/cli/shortcuts/common" + "github.com/spf13/cobra" +) + +func defaultConfig() *core.CliConfig { + return &core.CliConfig{ + AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu, + } +} + +func mountAndRun(t *testing.T, s common.Shortcut, args []string, f *cmdutil.Factory, stdout *bytes.Buffer) error { + t.Helper() + parent := &cobra.Command{Use: "test"} + s.Mount(parent, f) + parent.SetArgs(args) + parent.SilenceErrors = true + parent.SilenceUsage = true + if stdout != nil { + stdout.Reset() + } + return parent.Execute() +} + +func TestFeedSensitive_Validate_MissingEnableDisable(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, FeedSensitive, []string{ + "+sensitive", + "--feed-card-id", "oc_abc123", + "--user-ids", "ou_user1", + }, f, nil) + if err == nil { + t.Fatal("expected validation error, got nil") + } + if !strings.Contains(err.Error(), "--enable") || !strings.Contains(err.Error(), "--disable") { + t.Errorf("error should mention --enable and --disable, got: %v", err) + } +} + +func TestFeedSensitive_Validate_BothEnableAndDisable(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, FeedSensitive, []string{ + "+sensitive", + "--feed-card-id", "oc_abc123", + "--enable", + "--disable", + "--user-ids", "ou_user1", + }, f, nil) + if err == nil { + t.Fatal("expected validation error, got nil") + } + if !strings.Contains(err.Error(), "mutually exclusive") { + t.Errorf("error should mention mutually exclusive, got: %v", err) + } +} + +func TestFeedSensitive_Validate_InvalidFeedCardID(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, FeedSensitive, []string{ + "+sensitive", + "--feed-card-id", "invalid_id", + "--enable", + "--user-ids", "ou_user1", + }, f, nil) + if err == nil { + t.Fatal("expected validation error, got nil") + } + if !strings.Contains(err.Error(), "oc_") { + t.Errorf("error should mention oc_ prefix, got: %v", err) + } +} + +func TestFeedSensitive_Validate_EmptyUserIDs(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, FeedSensitive, []string{ + "+sensitive", + "--feed-card-id", "oc_abc123", + "--enable", + }, f, nil) + if err == nil { + t.Fatal("expected validation error for missing --user-ids, got nil") + } +} + +// Ensure the package compiles with context import used +var _ = context.Background From 57bd05855a67ded34528f37d5ea6f6e262db63d5 Mon Sep 17 00:00:00 2001 From: "maojinze.7" Date: Wed, 29 Apr 2026 17:12:35 +0800 Subject: [PATCH 04/10] fix(feed): clean up execute stub and strengthen validation tests --- shortcuts/feed/feed_sensitive.go | 8 ++------ shortcuts/feed/feed_sensitive_test.go | 7 +++---- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/shortcuts/feed/feed_sensitive.go b/shortcuts/feed/feed_sensitive.go index d75629d1c..3857fc42d 100644 --- a/shortcuts/feed/feed_sensitive.go +++ b/shortcuts/feed/feed_sensitive.go @@ -6,12 +6,10 @@ package feed import ( "context" "fmt" - "net/http" "github.com/larksuite/cli/internal/output" "github.com/larksuite/cli/internal/validate" "github.com/larksuite/cli/shortcuts/common" - larkcore "github.com/larksuite/oapi-sdk-go/v3/core" ) var FeedSensitive = common.Shortcut{ @@ -36,8 +34,8 @@ var FeedSensitive = common.Shortcut{ timeSensitive := runtime.Changed("enable") return common.NewDryRunAPI(). PATCH(fmt.Sprintf("/open-apis/im/v2/feed_cards/%s", validate.EncodePathSegment(feedCardID))). - Params(map[string]interface{}{"user_id_type": userIDType}). - Body(map[string]interface{}{ + Params(map[string]any{"user_id_type": userIDType}). + Body(map[string]any{ "time_sensitive": timeSensitive, "user_ids": userIDs, }) @@ -65,8 +63,6 @@ var FeedSensitive = common.Shortcut{ return nil }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { - _ = larkcore.QueryParams{} - _ = http.MethodPatch return nil // TODO: implement in Task 3 }, } diff --git a/shortcuts/feed/feed_sensitive_test.go b/shortcuts/feed/feed_sensitive_test.go index 1c0f252be..d32769006 100644 --- a/shortcuts/feed/feed_sensitive_test.go +++ b/shortcuts/feed/feed_sensitive_test.go @@ -5,7 +5,6 @@ package feed import ( "bytes" - "context" "strings" "testing" @@ -92,7 +91,7 @@ func TestFeedSensitive_Validate_EmptyUserIDs(t *testing.T) { if err == nil { t.Fatal("expected validation error for missing --user-ids, got nil") } + if !strings.Contains(err.Error(), "user-ids") { + t.Errorf("error should mention user-ids, got: %v", err) + } } - -// Ensure the package compiles with context import used -var _ = context.Background From 6f046505cf1364e9d456d5bc519af390b5951894 Mon Sep 17 00:00:00 2001 From: "maojinze.7" Date: Wed, 29 Apr 2026 17:17:10 +0800 Subject: [PATCH 05/10] feat(feed): implement execute with partial failure handling --- shortcuts/feed/feed_sensitive.go | 53 ++++++++++++- shortcuts/feed/feed_sensitive_test.go | 109 ++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 1 deletion(-) diff --git a/shortcuts/feed/feed_sensitive.go b/shortcuts/feed/feed_sensitive.go index 3857fc42d..676ae4740 100644 --- a/shortcuts/feed/feed_sensitive.go +++ b/shortcuts/feed/feed_sensitive.go @@ -6,6 +6,7 @@ package feed import ( "context" "fmt" + "io" "github.com/larksuite/cli/internal/output" "github.com/larksuite/cli/internal/validate" @@ -63,6 +64,56 @@ var FeedSensitive = common.Shortcut{ return nil }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { - return nil // TODO: implement in Task 3 + feedCardID := runtime.Str("feed-card-id") + userIDs := runtime.StrSlice("user-ids") + userIDType := runtime.Str("user-id-type") + timeSensitive := runtime.Changed("enable") + + resp, err := runtime.CallAPI("PATCH", + fmt.Sprintf("/open-apis/im/v2/feed_cards/%s", validate.EncodePathSegment(feedCardID)), + map[string]any{"user_id_type": userIDType}, + map[string]any{"time_sensitive": timeSensitive, "user_ids": userIDs}, + ) + if err != nil { + return err + } + + failedReasons := make([]map[string]any, 0) + if raw, ok := resp["failed_user_reasons"]; ok && raw != nil { + if slice, ok := raw.([]any); ok { + for _, item := range slice { + if m, ok := item.(map[string]any); ok { + failedReasons = append(failedReasons, m) + } + } + } + } + + outData := map[string]any{ + "feed_card_id": feedCardID, + "time_sensitive": timeSensitive, + "failed_user_reasons": failedReasons, + } + + runtime.OutFormat(outData, nil, func(w io.Writer) { + successCount := len(userIDs) - len(failedReasons) + if successCount > 0 { + fmt.Fprintf(w, "Time-sensitive updated for %d user(s) (feed_card_id: %s)\n", successCount, feedCardID) + } + if len(failedReasons) > 0 { + fmt.Fprintf(runtime.IO().ErrOut, "warning: %d user(s) failed:\n", len(failedReasons)) + for _, r := range failedReasons { + fmt.Fprintf(runtime.IO().ErrOut, " %v: %v\n", r["user_id"], r["error_message"]) + } + } + }) + + if len(failedReasons) > 0 { + if len(failedReasons) == len(userIDs) { + return output.Errorf(output.ExitAPI, "partial_failure", "all %d user(s) failed to update time-sensitive status", len(userIDs)) + } + return output.Errorf(output.ExitAPI, "partial_failure", "%d user(s) failed to update time-sensitive status", len(failedReasons)) + } + return nil }, } diff --git a/shortcuts/feed/feed_sensitive_test.go b/shortcuts/feed/feed_sensitive_test.go index d32769006..0c8ef46a2 100644 --- a/shortcuts/feed/feed_sensitive_test.go +++ b/shortcuts/feed/feed_sensitive_test.go @@ -10,6 +10,7 @@ import ( "github.com/larksuite/cli/internal/cmdutil" "github.com/larksuite/cli/internal/core" + "github.com/larksuite/cli/internal/httpmock" "github.com/larksuite/cli/shortcuts/common" "github.com/spf13/cobra" ) @@ -95,3 +96,111 @@ func TestFeedSensitive_Validate_EmptyUserIDs(t *testing.T) { t.Errorf("error should mention user-ids, got: %v", err) } } + +func TestFeedSensitive_Execute_AllSuccess(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig()) + reg.Register(&httpmock.Stub{ + Method: "PATCH", + URL: "/open-apis/im/v2/feed_cards/oc_abc123", + Body: map[string]interface{}{ + "code": 0, "msg": "success", + "data": map[string]interface{}{ + "failed_user_reasons": []interface{}{}, + }, + }, + }) + + err := mountAndRun(t, FeedSensitive, []string{ + "+sensitive", + "--feed-card-id", "oc_abc123", + "--enable", + "--user-ids", "ou_user1", + "--as", "bot", + "--format", "pretty", + }, f, stdout) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(stdout.String(), "Time-sensitive updated for 1 user(s)") { + t.Errorf("stdout should contain success message, got: %s", stdout.String()) + } +} + +func TestFeedSensitive_Execute_PartialFailure(t *testing.T) { + f, stdout, stderr, reg := cmdutil.TestFactory(t, defaultConfig()) + reg.Register(&httpmock.Stub{ + Method: "PATCH", + URL: "/open-apis/im/v2/feed_cards/oc_abc123", + Body: map[string]interface{}{ + "code": 0, "msg": "success", + "data": map[string]interface{}{ + "failed_user_reasons": []interface{}{ + map[string]interface{}{ + "error_code": 0, + "error_message": "The user is not in the chat", + "user_id": "ou_user2", + }, + }, + }, + }, + }) + + err := mountAndRun(t, FeedSensitive, []string{ + "+sensitive", + "--feed-card-id", "oc_abc123", + "--enable", + "--user-ids", "ou_user1,ou_user2", + "--as", "bot", + "--format", "pretty", + }, f, stdout) + + if err == nil { + t.Fatal("expected error for partial failure, got nil") + } + if !strings.Contains(stderr.String(), "warning:") { + t.Errorf("stderr should contain warning, got: %s", stderr.String()) + } + if !strings.Contains(stdout.String(), "Time-sensitive updated for 1 user(s)") { + t.Errorf("stdout should report 1 success, got: %s", stdout.String()) + } +} + +func TestFeedSensitive_Execute_AllFailed(t *testing.T) { + f, _, stderr, reg := cmdutil.TestFactory(t, defaultConfig()) + reg.Register(&httpmock.Stub{ + Method: "PATCH", + URL: "/open-apis/im/v2/feed_cards/oc_abc123", + Body: map[string]interface{}{ + "code": 0, "msg": "success", + "data": map[string]interface{}{ + "failed_user_reasons": []interface{}{ + map[string]interface{}{ + "error_code": 0, + "error_message": "The user is not in the chat", + "user_id": "ou_user1", + }, + }, + }, + }, + }) + + err := mountAndRun(t, FeedSensitive, []string{ + "+sensitive", + "--feed-card-id", "oc_abc123", + "--enable", + "--user-ids", "ou_user1", + "--as", "bot", + "--format", "pretty", + }, f, nil) + + if err == nil { + t.Fatal("expected error when all users fail, got nil") + } + if !strings.Contains(err.Error(), "all") { + t.Errorf("error should mention 'all', got: %v", err) + } + if !strings.Contains(stderr.String(), "warning:") { + t.Errorf("stderr should contain warning, got: %s", stderr.String()) + } +} From 82a2cef9c2691baf67eb2dcbb13569aecf1e343a Mon Sep 17 00:00:00 2001 From: "maojinze.7" Date: Wed, 29 Apr 2026 17:17:52 +0800 Subject: [PATCH 06/10] feat(feed): implement execute with partial failure handling --- shortcuts/feed/feed_sensitive_test.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/shortcuts/feed/feed_sensitive_test.go b/shortcuts/feed/feed_sensitive_test.go index 0c8ef46a2..e93748e6c 100644 --- a/shortcuts/feed/feed_sensitive_test.go +++ b/shortcuts/feed/feed_sensitive_test.go @@ -102,10 +102,10 @@ func TestFeedSensitive_Execute_AllSuccess(t *testing.T) { reg.Register(&httpmock.Stub{ Method: "PATCH", URL: "/open-apis/im/v2/feed_cards/oc_abc123", - Body: map[string]interface{}{ + Body: map[string]any{ "code": 0, "msg": "success", - "data": map[string]interface{}{ - "failed_user_reasons": []interface{}{}, + "data": map[string]any{ + "failed_user_reasons": []any{}, }, }, }) @@ -132,11 +132,11 @@ func TestFeedSensitive_Execute_PartialFailure(t *testing.T) { reg.Register(&httpmock.Stub{ Method: "PATCH", URL: "/open-apis/im/v2/feed_cards/oc_abc123", - Body: map[string]interface{}{ + Body: map[string]any{ "code": 0, "msg": "success", - "data": map[string]interface{}{ - "failed_user_reasons": []interface{}{ - map[string]interface{}{ + "data": map[string]any{ + "failed_user_reasons": []any{ + map[string]any{ "error_code": 0, "error_message": "The user is not in the chat", "user_id": "ou_user2", @@ -171,11 +171,11 @@ func TestFeedSensitive_Execute_AllFailed(t *testing.T) { reg.Register(&httpmock.Stub{ Method: "PATCH", URL: "/open-apis/im/v2/feed_cards/oc_abc123", - Body: map[string]interface{}{ + Body: map[string]any{ "code": 0, "msg": "success", - "data": map[string]interface{}{ - "failed_user_reasons": []interface{}{ - map[string]interface{}{ + "data": map[string]any{ + "failed_user_reasons": []any{ + map[string]any{ "error_code": 0, "error_message": "The user is not in the chat", "user_id": "ou_user1", From f9b43c48e86cb2182d4ad3b936bb4c1866f61698 Mon Sep 17 00:00:00 2001 From: "maojinze.7" Date: Wed, 29 Apr 2026 17:18:46 +0800 Subject: [PATCH 07/10] docs(feed): add lark-feed skill docs for +sensitive shortcut --- skills/lark-feed/SKILL.md | 38 ++++++++ .../references/lark-feed-sensitive.md | 90 +++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 skills/lark-feed/SKILL.md create mode 100644 skills/lark-feed/references/lark-feed-sensitive.md diff --git a/skills/lark-feed/SKILL.md b/skills/lark-feed/SKILL.md new file mode 100644 index 000000000..47e5963ab --- /dev/null +++ b/skills/lark-feed/SKILL.md @@ -0,0 +1,38 @@ +--- +name: lark-feed +version: 1.0.0 +description: "飞书消息流(feed):管理 Feed Card(群消息卡片)的即时提醒(时间敏感/置顶)状态。核心场景:为指定群聊的指定用户开启或关闭即时提醒。仅支持 bot 身份调用。" +metadata: + requires: + bins: ["lark-cli"] + cliHelp: "lark-cli feed --help" +--- + +# feed + +**CRITICAL — 开始前 MUST 先用 Read 工具读取 [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md),其中包含认证、权限处理** +**CRITICAL — 所有的 Shortcuts 在执行之前,务必先使用 Read 工具读取其对应的说明文档,禁止直接盲目调用命令。** +**CRITICAL — feed 域所有操作仅支持 bot 身份(`--as bot`),不支持 user 身份。** + +## 核心场景 + +### 1. 开启/关闭即时提醒 + +为群消息卡片的指定用户开启或关闭即时提醒(时间敏感/置顶)状态。 + +**MUST 先读取 [`references/lark-feed-sensitive.md`](references/lark-feed-sensitive.md)**,然后使用 `feed +sensitive`。 + +**典型用法:** +```bash +# 开启 +lark-cli feed +sensitive --feed-card-id oc_xxx --enable --user-ids ou_yyy + +# 关闭 +lark-cli feed +sensitive --feed-card-id oc_xxx --disable --user-ids ou_yyy +``` + +## Shortcuts + +| 命令 | 说明 | 文档 | +|------|------|------| +| `+sensitive` | 开启/关闭 Feed Card 即时提醒 | [lark-feed-sensitive.md](references/lark-feed-sensitive.md) | diff --git a/skills/lark-feed/references/lark-feed-sensitive.md b/skills/lark-feed/references/lark-feed-sensitive.md new file mode 100644 index 000000000..c7bce1dad --- /dev/null +++ b/skills/lark-feed/references/lark-feed-sensitive.md @@ -0,0 +1,90 @@ + +# feed +sensitive + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +为群消息卡片(Feed Card)的指定用户开启或关闭即时提醒(时间敏感/置顶)状态。仅支持群聊类型(oc_ 前缀),仅 bot 身份可调用。 + +需要的 scopes: ["im:datasync.feed_card.time_sensitive:write"] + +## 命令 + +```bash +# 为用户开启即时提醒 +lark-cli feed +sensitive --feed-card-id oc_xxx --enable --user-ids ou_yyy + +# 为多个用户开启(逗号分隔) +lark-cli feed +sensitive --feed-card-id oc_xxx --enable --user-ids ou_aaa,ou_bbb + +# 为多个用户开启(重复 flag) +lark-cli feed +sensitive --feed-card-id oc_xxx --enable --user-ids ou_aaa --user-ids ou_bbb + +# 关闭即时提醒 +lark-cli feed +sensitive --feed-card-id oc_xxx --disable --user-ids ou_yyy + +# 使用 union_id 类型 +lark-cli feed +sensitive --feed-card-id oc_xxx --enable --user-ids on_zzz --user-id-type union_id + +# 预览 API 调用(不实际执行) +lark-cli feed +sensitive --feed-card-id oc_xxx --enable --user-ids ou_yyy --dry-run +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--feed-card-id ` | 是 | Feed Card ID,必须以 `oc_` 开头,仅支持群聊 | +| `--enable` | 二选一 | 开启即时提醒(将卡片置顶给指定用户) | +| `--disable` | 二选一 | 关闭即时提醒 | +| `--user-ids ` | 是 | 用户 ID 列表,逗号分隔或重复 flag;用户须为该群聊成员 | +| `--user-id-type` | 否 | 用户 ID 类型:`open_id`(默认)\| `union_id` \| `user_id` | +| `--format` | 否 | 输出格式:`json`(默认)\| `pretty` | +| `--dry-run` | 否 | 预览 API 调用,不执行 | + +`--enable` 和 `--disable` 互斥,必须且只能提供其中一个。 + +## 输出格式 + +**JSON 模式(默认):** + +```json +{ + "feed_card_id": "oc_xxx", + "time_sensitive": true, + "failed_user_reasons": [ + { + "error_code": 0, + "error_message": "The user is not in the chat", + "user_id": "ou_yyy" + } + ] +} +``` + +**Pretty 模式(`--format pretty`):** + +全部成功时,stdout: +``` +Time-sensitive updated for 2 user(s) (feed_card_id: oc_xxx) +``` + +部分失败时,stdout 显示成功数,stderr 显示警告: +``` +warning: 1 user(s) failed: + ou_yyy: The user is not in the chat +``` + +## 退出码 + +| 条件 | 退出码 | +|------|--------| +| 全部成功 | 0 | +| 部分或全部用户失败 | 1 | +| 参数校验错误 | 1 | +| API 错误 | 1 | + +## 注意事项 + +- `--feed-card-id` 必须以 `oc_` 开头,否则报校验错误 +- 指定的用户须是该群聊的成员,非成员会出现在 `failed_user_reasons` 中 +- 跨租户用户通常不是群聊成员,会产生部分失败(非致命,仍返回退出码 1) From a5e2a26bcc612ddf3b8d0edbae51f8eec6748aa9 Mon Sep 17 00:00:00 2001 From: "maojinze.7" Date: Wed, 29 Apr 2026 17:34:30 +0800 Subject: [PATCH 08/10] test(feed): fix e2e fixture - use sandbox user instead of cross-app Mo --- .../feed/2026_04_29_feed_sensitive_test.go | 247 ++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 tests_e2e/feed/2026_04_29_feed_sensitive_test.go diff --git a/tests_e2e/feed/2026_04_29_feed_sensitive_test.go b/tests_e2e/feed/2026_04_29_feed_sensitive_test.go new file mode 100644 index 000000000..a5c0420be --- /dev/null +++ b/tests_e2e/feed/2026_04_29_feed_sensitive_test.go @@ -0,0 +1,247 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package feed + +import ( + "context" + "strings" + "testing" + "time" + + clie2e "github.com/larksuite/cli/tests/cli_e2e" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +// sandboxUserOpenID is the sandbox evaluation account's open_id in the bot's tenant. +// The user may not be a member of the dynamically-created group chat, so the API may +// return a failed_user_reasons entry. Tests accept that partial-failure outcome and +// verify the structural response (feed_card_id, time_sensitive fields present). +const sandboxUserOpenID = "ou_34efadf32063b955134c1fbea3f08bad" + +// TestFeedSensitive_EnableWorkflow proves the happy path for --enable: +// create a group chat (obtaining a real oc_ feed card ID), call +// feed +sensitive --enable, and assert the command returns exit 0 with a +// well-formed JSON envelope. +func TestFeedSensitive_EnableWorkflow(t *testing.T) { + parentT := t + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + t.Cleanup(cancel) + + suffix := clie2e.GenerateSuffix() + chatName := "lark-cli-e2e-feed-sensitive-" + suffix + + // Dynamically create a group chat to obtain a real oc_ chat_id. + // The chat_id serves as the feed_card_id for this test. + chatID := createChatForFeed(t, parentT, ctx, chatName) + + t.Run("enable time-sensitive as bot", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{ + "feed", "+sensitive", + "--feed-card-id", chatID, + "--enable", + "--user-ids", sandboxUserOpenID, + }, + DefaultAs: "bot", + }) + require.NoError(t, err) + // Exit 0 on full success, 1 on partial failure (user not in chat) — both acceptable. + assert.LessOrEqual(t, result.ExitCode, 1, + "exit code should be 0 or 1, got: %d, stderr:\n%s", result.ExitCode, result.Stderr) + + stdout := strings.TrimSpace(result.Stdout) + require.NotEmpty(t, stdout, "stdout should not be empty, stderr:\n%s", result.Stderr) + + // Spec output envelope: {"feed_card_id": "...", "time_sensitive": true, "failed_user_reasons": [...]} + feedCardID := gjson.Get(stdout, "feed_card_id").String() + assert.Equal(t, chatID, feedCardID, + "feed_card_id in response must match the requested chat, stdout:\n%s", stdout) + + timeSensitive := gjson.Get(stdout, "time_sensitive") + require.True(t, timeSensitive.Exists(), + "time_sensitive field must be present in response, stdout:\n%s", stdout) + assert.True(t, timeSensitive.Bool(), + "time_sensitive must be true for --enable, stdout:\n%s", stdout) + + failedReasons := gjson.Get(stdout, "failed_user_reasons") + if failedReasons.Exists() && len(failedReasons.Array()) > 0 { + t.Logf("INFO: partial failure (user not in chat, expected): %s", failedReasons.Raw) + } + }) +} + +// TestFeedSensitive_DisableWorkflow proves the happy path for --disable: +// verifies that passing --disable results in time_sensitive=false in the +// response envelope. +func TestFeedSensitive_DisableWorkflow(t *testing.T) { + parentT := t + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + t.Cleanup(cancel) + + suffix := clie2e.GenerateSuffix() + chatName := "lark-cli-e2e-feed-disable-" + suffix + + chatID := createChatForFeed(t, parentT, ctx, chatName) + + t.Run("disable time-sensitive as bot", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{ + "feed", "+sensitive", + "--feed-card-id", chatID, + "--disable", + "--user-ids", sandboxUserOpenID, + }, + DefaultAs: "bot", + }) + require.NoError(t, err) + // Exit 0 on full success, 1 on partial failure (user not in chat) — both acceptable. + assert.LessOrEqual(t, result.ExitCode, 1, + "exit code should be 0 or 1, got: %d, stderr:\n%s", result.ExitCode, result.Stderr) + + stdout := strings.TrimSpace(result.Stdout) + require.NotEmpty(t, stdout, "stdout should not be empty, stderr:\n%s", result.Stderr) + + feedCardID := gjson.Get(stdout, "feed_card_id").String() + assert.Equal(t, chatID, feedCardID, + "feed_card_id in response must match the requested chat, stdout:\n%s", stdout) + + timeSensitive := gjson.Get(stdout, "time_sensitive") + require.True(t, timeSensitive.Exists(), + "time_sensitive field must be present in response, stdout:\n%s", stdout) + assert.False(t, timeSensitive.Bool(), + "time_sensitive must be false for --disable, stdout:\n%s", stdout) + }) +} + +// TestFeedSensitive_DryRunFlagBehavior proves that --dry-run produces a PATCH +// request preview on stdout and exits 0 without making a real API call. +func TestFeedSensitive_DryRunFlagBehavior(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + t.Cleanup(cancel) + + t.Run("dry-run prints PATCH preview", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{ + "feed", "+sensitive", + "--feed-card-id", "oc_dryruntestid", + "--enable", + "--user-ids", sandboxUserOpenID, + "--dry-run", + }, + DefaultAs: "bot", + }) + require.NoError(t, err) + result.AssertExitCode(t, 0) + + combined := result.Stdout + result.Stderr + assert.Contains(t, combined, "PATCH", + "dry-run output must contain HTTP method PATCH, combined:\n%s", combined) + assert.Contains(t, combined, "/im/v2/feed_cards/", + "dry-run output must contain the feed_cards API path, combined:\n%s", combined) + assert.Contains(t, combined, "oc_dryruntestid", + "dry-run output must contain the feed_card_id path segment, combined:\n%s", combined) + }) +} + +// TestFeedSensitive_ValidationErrors proves the CLI validation layer: +// - missing --enable/--disable → exit non-zero with a descriptive error +// - both --enable and --disable together → exit non-zero (mutual exclusion) +// - invalid feed-card-id (no oc_ prefix) → exit non-zero with validation error +// +// The spec says exit 1 for validation errors; the CLI framework may return exit 2 +// for flag-parse failures. Both are accepted here as "not 0". +func TestFeedSensitive_ValidationErrors(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + t.Cleanup(cancel) + + t.Run("missing enable or disable flag", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{ + "feed", "+sensitive", + "--feed-card-id", "oc_testid", + "--user-ids", sandboxUserOpenID, + // intentionally omitting --enable and --disable + }, + DefaultAs: "bot", + }) + require.NoError(t, err) + assert.NotEqual(t, 0, result.ExitCode, + "must fail when neither --enable nor --disable is provided, stdout:\n%s\nstderr:\n%s", + result.Stdout, result.Stderr) + + combined := result.Stdout + result.Stderr + assert.Contains(t, combined, "--enable", + "error message must mention --enable flag, combined:\n%s", combined) + }) + + t.Run("enable and disable are mutually exclusive", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{ + "feed", "+sensitive", + "--feed-card-id", "oc_testid", + "--enable", + "--disable", + "--user-ids", sandboxUserOpenID, + }, + DefaultAs: "bot", + }) + require.NoError(t, err) + assert.NotEqual(t, 0, result.ExitCode, + "must fail when both --enable and --disable are provided, stdout:\n%s\nstderr:\n%s", + result.Stdout, result.Stderr) + + combined := result.Stdout + result.Stderr + assert.Contains(t, combined, "--disable", + "error message must mention --disable flag, combined:\n%s", combined) + }) + + t.Run("invalid feed-card-id without oc_ prefix", func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{ + "feed", "+sensitive", + "--feed-card-id", "invalid_id_no_oc_prefix", + "--enable", + "--user-ids", sandboxUserOpenID, + }, + DefaultAs: "bot", + }) + require.NoError(t, err) + assert.NotEqual(t, 0, result.ExitCode, + "must fail for feed-card-id that does not start with oc_, stdout:\n%s\nstderr:\n%s", + result.Stdout, result.Stderr) + + combined := result.Stdout + result.Stderr + assert.Contains(t, combined, "oc_", + "error message must mention the oc_ prefix requirement, combined:\n%s", combined) + }) +} + +// createChatForFeed creates a private group chat via im +chat-create and +// returns the chat_id (which doubles as the feed_card_id for feed tests). +// No cleanup is registered because lark-cli im has no chat-delete command. +func createChatForFeed(t *testing.T, parentT *testing.T, ctx context.Context, name string) string { + t.Helper() + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{ + "im", "+chat-create", + "--name", name, + "--type", "private", + }, + DefaultAs: "bot", + }) + require.NoError(t, err) + result.AssertExitCode(t, 0) + result.AssertStdoutStatus(t, true) + + chatID := gjson.Get(result.Stdout, "data.chat_id").String() + require.NotEmpty(t, chatID, "chat_id must not be empty, stdout:\n%s", result.Stdout) + + // No chat-delete command exists in lark-cli im; chats are left in the account. + _ = parentT + + return chatID +} From 277cdf351f5c165ea43b6cd7f35039ee80811df6 Mon Sep 17 00:00:00 2001 From: "maojinze.7" Date: Wed, 29 Apr 2026 17:35:34 +0800 Subject: [PATCH 09/10] test(feed): fix e2e gjson paths to use data envelope wrapper --- tests_e2e/feed/2026_04_29_feed_sensitive_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests_e2e/feed/2026_04_29_feed_sensitive_test.go b/tests_e2e/feed/2026_04_29_feed_sensitive_test.go index a5c0420be..67ea2fe10 100644 --- a/tests_e2e/feed/2026_04_29_feed_sensitive_test.go +++ b/tests_e2e/feed/2026_04_29_feed_sensitive_test.go @@ -56,17 +56,17 @@ func TestFeedSensitive_EnableWorkflow(t *testing.T) { require.NotEmpty(t, stdout, "stdout should not be empty, stderr:\n%s", result.Stderr) // Spec output envelope: {"feed_card_id": "...", "time_sensitive": true, "failed_user_reasons": [...]} - feedCardID := gjson.Get(stdout, "feed_card_id").String() + feedCardID := gjson.Get(stdout, "data.feed_card_id").String() assert.Equal(t, chatID, feedCardID, "feed_card_id in response must match the requested chat, stdout:\n%s", stdout) - timeSensitive := gjson.Get(stdout, "time_sensitive") + timeSensitive := gjson.Get(stdout, "data.time_sensitive") require.True(t, timeSensitive.Exists(), "time_sensitive field must be present in response, stdout:\n%s", stdout) assert.True(t, timeSensitive.Bool(), "time_sensitive must be true for --enable, stdout:\n%s", stdout) - failedReasons := gjson.Get(stdout, "failed_user_reasons") + failedReasons := gjson.Get(stdout, "data.failed_user_reasons") if failedReasons.Exists() && len(failedReasons.Array()) > 0 { t.Logf("INFO: partial failure (user not in chat, expected): %s", failedReasons.Raw) } @@ -104,11 +104,11 @@ func TestFeedSensitive_DisableWorkflow(t *testing.T) { stdout := strings.TrimSpace(result.Stdout) require.NotEmpty(t, stdout, "stdout should not be empty, stderr:\n%s", result.Stderr) - feedCardID := gjson.Get(stdout, "feed_card_id").String() + feedCardID := gjson.Get(stdout, "data.feed_card_id").String() assert.Equal(t, chatID, feedCardID, "feed_card_id in response must match the requested chat, stdout:\n%s", stdout) - timeSensitive := gjson.Get(stdout, "time_sensitive") + timeSensitive := gjson.Get(stdout, "data.time_sensitive") require.True(t, timeSensitive.Exists(), "time_sensitive field must be present in response, stdout:\n%s", stdout) assert.False(t, timeSensitive.Bool(), From 715b2daa12bcf4c0b5a0e2e055ada7b844090117 Mon Sep 17 00:00:00 2001 From: "maojinze.7" Date: Wed, 29 Apr 2026 17:56:38 +0800 Subject: [PATCH 10/10] test(feed): add user to chat before feed +sensitive e2e tests --- .../feed/2026_04_29_feed_sensitive_test.go | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/tests_e2e/feed/2026_04_29_feed_sensitive_test.go b/tests_e2e/feed/2026_04_29_feed_sensitive_test.go index 67ea2fe10..b52015b07 100644 --- a/tests_e2e/feed/2026_04_29_feed_sensitive_test.go +++ b/tests_e2e/feed/2026_04_29_feed_sensitive_test.go @@ -16,9 +16,8 @@ import ( ) // sandboxUserOpenID is the sandbox evaluation account's open_id in the bot's tenant. -// The user may not be a member of the dynamically-created group chat, so the API may -// return a failed_user_reasons entry. Tests accept that partial-failure outcome and -// verify the structural response (feed_card_id, time_sensitive fields present). +// This user is added to the group chat before feed +sensitive tests, ensuring the +// happy path succeeds with zero failed_user_reasons. const sandboxUserOpenID = "ou_34efadf32063b955134c1fbea3f08bad" // TestFeedSensitive_EnableWorkflow proves the happy path for --enable: @@ -36,6 +35,7 @@ func TestFeedSensitive_EnableWorkflow(t *testing.T) { // Dynamically create a group chat to obtain a real oc_ chat_id. // The chat_id serves as the feed_card_id for this test. chatID := createChatForFeed(t, parentT, ctx, chatName) + addUserToChat(t, ctx, chatID, sandboxUserOpenID) t.Run("enable time-sensitive as bot", func(t *testing.T) { result, err := clie2e.RunCmd(ctx, clie2e.Request{ @@ -48,14 +48,11 @@ func TestFeedSensitive_EnableWorkflow(t *testing.T) { DefaultAs: "bot", }) require.NoError(t, err) - // Exit 0 on full success, 1 on partial failure (user not in chat) — both acceptable. - assert.LessOrEqual(t, result.ExitCode, 1, - "exit code should be 0 or 1, got: %d, stderr:\n%s", result.ExitCode, result.Stderr) + result.AssertExitCode(t, 0) stdout := strings.TrimSpace(result.Stdout) require.NotEmpty(t, stdout, "stdout should not be empty, stderr:\n%s", result.Stderr) - // Spec output envelope: {"feed_card_id": "...", "time_sensitive": true, "failed_user_reasons": [...]} feedCardID := gjson.Get(stdout, "data.feed_card_id").String() assert.Equal(t, chatID, feedCardID, "feed_card_id in response must match the requested chat, stdout:\n%s", stdout) @@ -67,9 +64,8 @@ func TestFeedSensitive_EnableWorkflow(t *testing.T) { "time_sensitive must be true for --enable, stdout:\n%s", stdout) failedReasons := gjson.Get(stdout, "data.failed_user_reasons") - if failedReasons.Exists() && len(failedReasons.Array()) > 0 { - t.Logf("INFO: partial failure (user not in chat, expected): %s", failedReasons.Raw) - } + assert.Equal(t, 0, len(failedReasons.Array()), + "failed_user_reasons must be empty when user is in chat, stdout:\n%s", stdout) }) } @@ -85,6 +81,7 @@ func TestFeedSensitive_DisableWorkflow(t *testing.T) { chatName := "lark-cli-e2e-feed-disable-" + suffix chatID := createChatForFeed(t, parentT, ctx, chatName) + addUserToChat(t, ctx, chatID, sandboxUserOpenID) t.Run("disable time-sensitive as bot", func(t *testing.T) { result, err := clie2e.RunCmd(ctx, clie2e.Request{ @@ -97,9 +94,7 @@ func TestFeedSensitive_DisableWorkflow(t *testing.T) { DefaultAs: "bot", }) require.NoError(t, err) - // Exit 0 on full success, 1 on partial failure (user not in chat) — both acceptable. - assert.LessOrEqual(t, result.ExitCode, 1, - "exit code should be 0 or 1, got: %d, stderr:\n%s", result.ExitCode, result.Stderr) + result.AssertExitCode(t, 0) stdout := strings.TrimSpace(result.Stdout) require.NotEmpty(t, stdout, "stdout should not be empty, stderr:\n%s", result.Stderr) @@ -113,6 +108,10 @@ func TestFeedSensitive_DisableWorkflow(t *testing.T) { "time_sensitive field must be present in response, stdout:\n%s", stdout) assert.False(t, timeSensitive.Bool(), "time_sensitive must be false for --disable, stdout:\n%s", stdout) + + failedReasons := gjson.Get(stdout, "data.failed_user_reasons") + assert.Equal(t, 0, len(failedReasons.Array()), + "failed_user_reasons must be empty when user is in chat, stdout:\n%s", stdout) }) } @@ -219,6 +218,22 @@ func TestFeedSensitive_ValidationErrors(t *testing.T) { }) } +// addUserToChat adds a user to a group chat via im chat.members create (bot identity). +// Required before feed +sensitive tests so the user is a valid chat member. +func addUserToChat(t *testing.T, ctx context.Context, chatID, userOpenID string) { + t.Helper() + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{ + "im", "chat.members", "create", + "--params", `{"chat_id":"` + chatID + `"}`, + "--data", `{"id_list":["` + userOpenID + `"]}`, + }, + DefaultAs: "bot", + }) + require.NoError(t, err) + result.AssertExitCode(t, 0) +} + // createChatForFeed creates a private group chat via im +chat-create and // returns the chat_id (which doubles as the feed_card_id for feed tests). // No cleanup is registered because lark-cli im has no chat-delete command.