-
Notifications
You must be signed in to change notification settings - Fork 582
[harness:01KP342QM8XZZT86SCJJGGK50Y-4] Mail CLI 支持定时发送 — CLI 技术方案 #452
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| // Copyright (c) 2026 Lark Technologies Pte. Ltd. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package mail | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "net/url" | ||
|
|
||
| "github.com/larksuite/cli/internal/output" | ||
| "github.com/larksuite/cli/shortcuts/common" | ||
| ) | ||
|
|
||
| var MailCancelScheduledSend = common.Shortcut{ | ||
| Service: "mail", | ||
| Command: "+cancel-scheduled-send", | ||
| Description: "Cancel a scheduled email send. The email will be restored as a draft.", | ||
| Risk: "write", | ||
| Scopes: []string{"mail:user_mailbox.message:send"}, | ||
| AuthTypes: []string{"user"}, | ||
| HasFormat: true, | ||
| Flags: []common.Flag{ | ||
| {Name: "message-id", Desc: "Message ID of the scheduled email to cancel (required)", Required: true}, | ||
| {Name: "user-mailbox-id", Desc: "User mailbox ID (default: me)"}, | ||
| }, | ||
| Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| if runtime.Str("message-id") == "" { | ||
| return output.ErrValidation("--message-id is required") | ||
| } | ||
| return nil | ||
| }, | ||
| DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { | ||
| messageID := runtime.Str("message-id") | ||
| userMailboxID := runtime.Str("user-mailbox-id") | ||
| if userMailboxID == "" { | ||
| userMailboxID = "me" | ||
| } | ||
| return common.NewDryRunAPI(). | ||
| Desc("Cancel scheduled send — message will be restored as draft"). | ||
| POST(fmt.Sprintf("/open-apis/mail/v1/user_mailboxes/%s/messages/%s/cancel_scheduled_send", | ||
| url.PathEscape(userMailboxID), | ||
| url.PathEscape(messageID), | ||
| )) | ||
| }, | ||
| Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { | ||
| messageID := runtime.Str("message-id") | ||
| userMailboxID := runtime.Str("user-mailbox-id") | ||
| if userMailboxID == "" { | ||
| userMailboxID = "me" | ||
| } | ||
|
|
||
| path := fmt.Sprintf("/open-apis/mail/v1/user_mailboxes/%s/messages/%s/cancel_scheduled_send", | ||
| url.PathEscape(userMailboxID), | ||
| url.PathEscape(messageID), | ||
| ) | ||
|
|
||
| _, err := runtime.CallAPI("POST", path, nil, nil) | ||
| if err != nil { | ||
| return output.ErrWithHint(output.ExitAPI, "api_error", | ||
| fmt.Sprintf("Failed to cancel scheduled send for message %s", messageID), | ||
| "Ensure the message ID is correct and the email has not already been sent.", | ||
| ) | ||
| } | ||
|
|
||
| runtime.Out(map[string]interface{}{ | ||
| "message_id": messageID, | ||
| "status": "cancelled", | ||
| "restored_as_draft": true, | ||
| }, nil) | ||
|
|
||
| fmt.Fprintf(runtime.IO().ErrOut, | ||
| "tip: the message has been restored as a draft. Use lark-cli mail +draft-edit --id %s to edit.\n", | ||
| sanitizeForTerminal(messageID)) | ||
|
Comment on lines
+72
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
draft_edit_file="$(rg -n --type=go 'Command:\s*"\+draft-edit"' | cut -d: -f1 | head -n1)"
if [ -z "${draft_edit_file}" ]; then
echo "Could not find the +draft-edit shortcut definition" >&2
exit 1
fi
echo "=== +draft-edit shortcut definition: ${draft_edit_file} ==="
sed -n '1,240p' "${draft_edit_file}"
echo
echo "=== mailbox-related flags/path usage in ${draft_edit_file} ==="
rg -n 'Name:\s*"user-mailbox-id"|mailboxPath\(|runtime\.Str\("user-mailbox-id"\)' "${draft_edit_file}"Repository: larksuite/cli Length of output: 12017 🏁 Script executed: cat -n shortcuts/mail/mail_cancel_scheduled_send.go | sed -n '50,90p'Repository: larksuite/cli Length of output: 1093 🏁 Script executed: rg -n "messageID|mailboxID|resolveComposeMailboxID" shortcuts/mail/mail_cancel_scheduled_send.go | head -20Repository: larksuite/cli Length of output: 368 🏁 Script executed: # Search for similar hint patterns in mail shortcuts that might include mailbox context
rg -n "Use lark-cli mail" shortcuts/mail/ -A 1 -B 1Repository: larksuite/cli Length of output: 372 🏁 Script executed: cat -n shortcuts/mail/mail_cancel_scheduled_send.go | sed -n '35,65p'Repository: larksuite/cli Length of output: 1335 🏁 Script executed: rg -n "MailCancelScheduledSend\s*=" shortcuts/mail/mail_cancel_scheduled_send.go -A 30 | head -50Repository: larksuite/cli Length of output: 1378 🏁 Script executed: rg -n 'Name:\s*"draft-id"|Name:\s*"mailbox"|Name:\s*"from"' shortcuts/mail/mail_draft_edit.go | head -10Repository: larksuite/cli Length of output: 548 Include mailbox context in the draft-edit hint and use the correct flag name. Line 73 has two issues. The hint uses The hint should include the mailbox context (captured from Suggested fixfmt.Fprintf(runtime.IO().ErrOut,
"tip: the message has been restored as a draft. Use lark-cli mail +draft-edit --draft-id %s --mailbox %s to edit.\n",
sanitizeForTerminal(messageID), sanitizeForTerminal(userMailboxID))🤖 Prompt for AI Agents |
||
|
|
||
| return nil | ||
| }, | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| // Copyright (c) 2026 Lark Technologies Pte. Ltd. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package mail | ||
|
|
||
| import ( | ||
| "errors" | ||
| "testing" | ||
|
|
||
| "github.com/larksuite/cli/internal/httpmock" | ||
| "github.com/larksuite/cli/internal/output" | ||
| ) | ||
|
|
||
| func TestCancelScheduledSend_Success(t *testing.T) { | ||
| f, stdout, _, reg := mailShortcutTestFactory(t) | ||
| reg.Register(&httpmock.Stub{ | ||
| URL: "/user_mailboxes/me/messages/msg_sched_001/cancel_scheduled_send", | ||
| Body: map[string]interface{}{ | ||
| "code": 0, | ||
| "data": map[string]interface{}{}, | ||
| }, | ||
| }) | ||
|
|
||
| err := runMountedMailShortcut(t, MailCancelScheduledSend, []string{ | ||
| "+cancel-scheduled-send", "--message-id", "msg_sched_001", | ||
| }, f, stdout) | ||
| if err != nil { | ||
| t.Fatalf("expected no error, got: %v", err) | ||
| } | ||
|
|
||
| data := decodeShortcutEnvelopeData(t, stdout) | ||
| if data["message_id"] != "msg_sched_001" { | ||
| t.Errorf("expected message_id msg_sched_001, got %v", data["message_id"]) | ||
| } | ||
| if data["status"] != "cancelled" { | ||
| t.Errorf("expected status cancelled, got %v", data["status"]) | ||
| } | ||
| if data["restored_as_draft"] != true { | ||
| t.Errorf("expected restored_as_draft true, got %v", data["restored_as_draft"]) | ||
| } | ||
| } | ||
|
|
||
| func TestCancelScheduledSend_MissingMessageID(t *testing.T) { | ||
| f, stdout, _, _ := mailShortcutTestFactory(t) | ||
| err := runMountedMailShortcut(t, MailCancelScheduledSend, []string{ | ||
| "+cancel-scheduled-send", | ||
| }, f, stdout) | ||
| if err == nil { | ||
| t.Fatal("expected error for missing --message-id, got nil") | ||
| } | ||
| } | ||
|
|
||
| func TestCancelScheduledSend_APIError(t *testing.T) { | ||
| f, stdout, _, reg := mailShortcutTestFactory(t) | ||
| reg.Register(&httpmock.Stub{ | ||
| URL: "/user_mailboxes/me/messages/msg_invalid/cancel_scheduled_send", | ||
| Body: map[string]interface{}{ | ||
| "code": 99991, | ||
| "msg": "message not found", | ||
| }, | ||
| }) | ||
|
|
||
| err := runMountedMailShortcut(t, MailCancelScheduledSend, []string{ | ||
| "+cancel-scheduled-send", "--message-id", "msg_invalid", | ||
| }, f, stdout) | ||
| if err == nil { | ||
| t.Fatal("expected error for API failure, got nil") | ||
| } | ||
| var exitErr *output.ExitError | ||
| if !errors.As(err, &exitErr) { | ||
| t.Fatalf("expected ExitError, got %T: %v", err, err) | ||
| } | ||
| } | ||
|
|
||
| func TestCancelScheduledSend_CustomMailboxID(t *testing.T) { | ||
| f, stdout, _, reg := mailShortcutTestFactory(t) | ||
| reg.Register(&httpmock.Stub{ | ||
| URL: "/user_mailboxes/mailbox_abc/messages/msg_sched_002/cancel_scheduled_send", | ||
| Body: map[string]interface{}{ | ||
| "code": 0, | ||
| "data": map[string]interface{}{}, | ||
| }, | ||
| }) | ||
|
|
||
| err := runMountedMailShortcut(t, MailCancelScheduledSend, []string{ | ||
| "+cancel-scheduled-send", "--message-id", "msg_sched_002", "--user-mailbox-id", "mailbox_abc", | ||
| }, f, stdout) | ||
| if err != nil { | ||
| t.Fatalf("expected no error, got: %v", err) | ||
| } | ||
|
|
||
| data := decodeShortcutEnvelopeData(t, stdout) | ||
| if data["message_id"] != "msg_sched_002" { | ||
| t.Errorf("expected message_id msg_sched_002, got %v", data["message_id"]) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
runtime.CallAPIreturns an error, the originalerris silently dropped and replaced with a generic hint. Other shortcuts in this package wrap the underlying error withfmt.Errorf("...: %w", err), which preserves the diagnostic message for debugging. Consider wrappingerrhere so that the actual API error code/message is still surfaced: