diff --git a/shortcuts/common/runner.go b/shortcuts/common/runner.go index 5a6e2ba49..51ec9baa1 100644 --- a/shortcuts/common/runner.go +++ b/shortcuts/common/runner.go @@ -225,6 +225,20 @@ func (ctx *RuntimeContext) DoAPI(req *larkcore.ApiReq, opts ...larkcore.RequestO return ac.DoSDKRequest(ctx.ctx, req, ctx.As(), opts...) } +// DoAPIAsBot executes a raw Lark SDK request using bot identity (tenant access token), +// regardless of the current --as flag. Use this for bot-only APIs (e.g. image/file upload) +// that must be called with TAT even when the surrounding shortcut runs as user. +func (ctx *RuntimeContext) DoAPIAsBot(req *larkcore.ApiReq, opts ...larkcore.RequestOptionFunc) (*larkcore.ApiResp, error) { + ac, err := ctx.getAPIClient() + if err != nil { + return nil, err + } + if optFn := cmdutil.ShortcutHeaderOpts(ctx.ctx); optFn != nil { + opts = append(opts, optFn) + } + return ac.DoSDKRequest(ctx.ctx, req, core.AsBot, opts...) +} + type cancelOnCloseReadCloser struct { io.ReadCloser cancel context.CancelFunc diff --git a/shortcuts/im/helpers.go b/shortcuts/im/helpers.go index 4c3580573..32a0a33f5 100644 --- a/shortcuts/im/helpers.go +++ b/shortcuts/im/helpers.go @@ -876,7 +876,7 @@ func uploadImageToIM(ctx context.Context, runtime *common.RuntimeContext, filePa fd.AddField("image_type", imageType) fd.AddFile("image", f) - apiResp, err := runtime.DoAPI(&larkcore.ApiReq{ + apiResp, err := runtime.DoAPIAsBot(&larkcore.ApiReq{ HttpMethod: http.MethodPost, ApiPath: "/open-apis/im/v1/images", Body: fd, @@ -922,7 +922,7 @@ func uploadFileToIM(ctx context.Context, runtime *common.RuntimeContext, filePat } fd.AddFile("file", f) - apiResp, err := runtime.DoAPI(&larkcore.ApiReq{ + apiResp, err := runtime.DoAPIAsBot(&larkcore.ApiReq{ HttpMethod: http.MethodPost, ApiPath: "/open-apis/im/v1/files", Body: fd, diff --git a/shortcuts/im/im_messages_reply.go b/shortcuts/im/im_messages_reply.go index d7aab26b4..30087d589 100644 --- a/shortcuts/im/im_messages_reply.go +++ b/shortcuts/im/im_messages_reply.go @@ -17,10 +17,12 @@ import ( var ImMessagesReply = common.Shortcut{ Service: "im", Command: "+messages-reply", - Description: "Reply to a message (supports thread replies) with bot identity; bot-only; supports text/markdown/post/media replies, reply-in-thread, idempotency key", + Description: "Reply to a message (supports thread replies); user/bot; supports text/markdown/post/media replies, reply-in-thread, idempotency key", Risk: "write", Scopes: []string{"im:message:send_as_bot"}, - AuthTypes: []string{"bot"}, + UserScopes: []string{"im:message.send_as_user"}, + BotScopes: []string{"im:message:send_as_bot"}, + AuthTypes: []string{"bot", "user"}, 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"}}, diff --git a/shortcuts/im/im_messages_send.go b/shortcuts/im/im_messages_send.go index 5e9fa2c9a..f3364c54e 100644 --- a/shortcuts/im/im_messages_send.go +++ b/shortcuts/im/im_messages_send.go @@ -18,10 +18,12 @@ import ( var ImMessagesSend = common.Shortcut{ Service: "im", Command: "+messages-send", - Description: "Send a message to a chat or direct message with bot identity; bot-only; sends to chat-id or user-id with text/markdown/post/media, supports idempotency key", + Description: "Send a message to a chat or direct message; user/bot; sends to chat-id or user-id with text/markdown/post/media, supports idempotency key", Risk: "write", Scopes: []string{"im:message:send_as_bot"}, - AuthTypes: []string{"bot"}, + UserScopes: []string{"im:message.send_as_user"}, + BotScopes: []string{"im:message:send_as_bot"}, + AuthTypes: []string{"bot", "user"}, Flags: []common.Flag{ {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)"}, diff --git a/skills/lark-im/SKILL.md b/skills/lark-im/SKILL.md index c7f86dd24..72596c48d 100644 --- a/skills/lark-im/SKILL.md +++ b/skills/lark-im/SKILL.md @@ -61,10 +61,10 @@ Shortcut 是对常用操作的高级封装(`lark-cli im + [flags]`)。 | [`+chat-search`](references/lark-im-chat-search.md) | Search visible group chats by keyword and/or member open_ids (e.g. look up chat_id by group name); user/bot; supports member/type filters, sorting, and pagination | | [`+chat-update`](references/lark-im-chat-update.md) | Update group chat name or description; user/bot; updates a chat's name or description | | [`+messages-mget`](references/lark-im-messages-mget.md) | Batch get messages by IDs; user/bot; fetches up to 50 om_ message IDs, formats sender names, expands thread replies | -| [`+messages-reply`](references/lark-im-messages-reply.md) | Reply to a message (supports thread replies) with bot identity; bot-only; supports text/markdown/post/media replies, reply-in-thread, idempotency key | +| [`+messages-reply`](references/lark-im-messages-reply.md) | Reply to a message (supports thread replies); user/bot; supports text/markdown/post/media replies, reply-in-thread, idempotency key | | [`+messages-resources-download`](references/lark-im-messages-resources-download.md) | Download images/files from a message; user/bot; downloads image/file resources by message-id and file-key to a safe relative output path | | [`+messages-search`](references/lark-im-messages-search.md) | Search messages across chats (supports keyword, sender, time range filters) with user identity; user-only; filters by chat/sender/attachment/time, supports auto-pagination via `--page-all` / `--page-limit`, enriches results via batched mget and chats batch_query | -| [`+messages-send`](references/lark-im-messages-send.md) | Send a message to a chat or direct message with bot identity; bot-only; sends to chat-id or user-id with text/markdown/post/media, supports idempotency key | +| [`+messages-send`](references/lark-im-messages-send.md) | Send a message to a chat or direct message; user/bot; sends to chat-id or user-id with text/markdown/post/media, supports idempotency key | | [`+threads-messages-list`](references/lark-im-threads-messages-list.md) | List messages in a thread; user/bot; accepts om_/omt_ input, resolves message IDs to thread_id, supports sort/pagination | ## API Resources diff --git a/skills/lark-im/references/lark-im-messages-reply.md b/skills/lark-im/references/lark-im-messages-reply.md index 8d4e007ba..5b8cd8b94 100644 --- a/skills/lark-im/references/lark-im-messages-reply.md +++ b/skills/lark-im/references/lark-im-messages-reply.md @@ -2,7 +2,7 @@ > **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) first to understand authentication, global parameters, and safety rules. -Reply to a specific message. Only supports bot identity. Also supports thread replies. +Reply to a specific message. Supports both user identity (`--as user`) and bot identity (`--as bot`). Also supports thread replies. This skill maps to the shortcut: `lark-cli im +messages-reply` (internally calls `POST /open-apis/im/v1/messages/:message_id/reply`). @@ -12,12 +12,14 @@ Replies sent by this tool are visible to other people. Before calling it, you ** 1. Which message to reply to 2. The reply content -3. Which identity to use (bot only) +3. Which identity to use (user or bot) **Do not** send a reply without explicit user approval. 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. +When using `--as user`, the reply is sent as the authorized end user and requires the `im:message.send_as_user` scope. + ## Choose The Right Content Flag | Need | Recommended flag | Why | @@ -152,7 +154,7 @@ lark-cli im +messages-reply --message-id om_xxx --markdown $'## Test\n\nhello' - | `--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 | +| `--as ` | 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`/`--audio` cannot be used together. Media flags are also mutually exclusive with each other. @@ -209,11 +211,12 @@ The reply appears in the target message's thread and does not show up in the mai - 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 +- `--image`/`--file`/`--video`/`--audio`/`--video-cover` support local file paths; the shortcut uploads first and then sends the reply; file/image upload is bot-only, so when using `--as user`, the upload step is automatically performed with bot identity, and only the final send uses user identity - 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 \ No newline at end of file +- `--as user` uses a user access token (UAT) and requires the `im:message.send_as_user` scope; the reply is sent as the authorized end user +- `--as bot` uses a tenant access token (TAT), and requires the `im:message:send_as_bot` scope diff --git a/skills/lark-im/references/lark-im-messages-send.md b/skills/lark-im/references/lark-im-messages-send.md index c735c75a4..849eff009 100644 --- a/skills/lark-im/references/lark-im-messages-send.md +++ b/skills/lark-im/references/lark-im-messages-send.md @@ -2,7 +2,7 @@ > **Prerequisite:** Read [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) first to understand authentication, global parameters, and safety rules. -Send a message to a group chat or a direct message conversation. Only supports bot identity. +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`). @@ -12,12 +12,14 @@ Messages sent by this tool are visible to other people. Before calling it, you * 1. The recipient (which person or which group) 2. The message content -3. The sending identity (bot only) +3. 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` scope. + ## Choose The Right Content Flag | Need | Recommended flag | Why | @@ -158,7 +160,7 @@ lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Test\n\nhello' --dry | `--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 | +| `--as ` | 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`/`--audio` cannot be used together. Media flags are also mutually exclusive with each other. @@ -209,12 +211,13 @@ lark-cli im +messages-send --chat-id oc_xxx --markdown $'## Test\n\nhello' --dry - `--chat-id` and `--user-id` are mutually exclusive; you must provide exactly one - `--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 +- `--image`/`--file`/`--video`/`--audio` support local file paths; the shortcut uploads first and then sends the message; file/image upload is bot-only, so when using `--as user`, the upload step is automatically performed with bot identity, and only the final send uses user identity - 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 user` uses a user access token (UAT) and requires the `im:message.send_as_user` scope; the message is sent as the authorized end user - `--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 \ No newline at end of file +- 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