From d32bbe009dd638d8a455d2d98ca97efac54f52bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=95=BF=E7=9C=9F?= Date: Thu, 13 Aug 2026 13:28:00 +0800 Subject: [PATCH] fix(chat): expose idempotency key for message send --- internal/app/param_alias_e2e_test.go | 25 ++++++++++++++++++- .../param_alias_payload_equivalence_test.go | 6 ++--- internal/app/root_help_test.go | 8 ++++++ internal/cli/schema_catalog_test.go | 9 +++++++ internal/helpers/chat.go | 7 ++++-- 5 files changed, 49 insertions(+), 6 deletions(-) diff --git a/internal/app/param_alias_e2e_test.go b/internal/app/param_alias_e2e_test.go index 8267390c..a14170de 100644 --- a/internal/app/param_alias_e2e_test.go +++ b/internal/app/param_alias_e2e_test.go @@ -324,7 +324,7 @@ func TestCrossPlatformCoverageParamAliasWriteCommandFinalPayload(t *testing.T) { "chat", "message", "send", "--to-user", "D-recipient", "--text", "hello alias", - "--uuid", "alias-e2e", + "--idempotency-key", "alias-e2e", ) if err != nil { t.Fatalf("chat write alias E2E error = %v", err) @@ -350,6 +350,29 @@ func TestCrossPlatformCoverageParamAliasWriteCommandFinalPayload(t *testing.T) { } } +func TestCrossPlatformCoverageChatMessageSendLegacyUUIDAliasFinalPayload(t *testing.T) { + caller := ¶mAliasCaptureCaller{} + _, err := executeParamAliasE2E(t, caller, + "chat", "message", "send", + "--group", "fixture-conversation", + "--text", "hello legacy uuid", + "--uuid", "legacy-alias-e2e", + ) + if err != nil { + t.Fatalf("chat message send legacy uuid error = %v", err) + } + if len(caller.calls) != 1 || caller.calls[0].tool != "send_personal_message" { + t.Fatalf("chat calls = %#v", caller.calls) + } + payload := caller.calls[0].args + if payload["uuid"] != "legacy-alias-e2e" || payload["openConversationId"] != "fixture-conversation" { + t.Fatalf("chat legacy uuid payload = %#v", payload) + } + if _, exists := payload["idempotency-key"]; exists { + t.Fatalf("chat payload leaked CLI-only idempotency-key: %#v", payload) + } +} + func TestCrossPlatformCoverageChatReactionConversationAliasesReachCanonicalPayload(t *testing.T) { tests := []struct { name string diff --git a/internal/app/param_alias_payload_equivalence_test.go b/internal/app/param_alias_payload_equivalence_test.go index 494a8b6c..0db9b2dc 100644 --- a/internal/app/param_alias_payload_equivalence_test.go +++ b/internal/app/param_alias_payload_equivalence_test.go @@ -88,7 +88,7 @@ var paramAliasCompleteCommands = map[string][]string{ "chat message recall": {"chat", "message", "recall", "--conversation-id", "fixture-conversation", "--msg-id", "message-1", "--yes"}, "chat message reply": {"chat", "message", "reply", "--conversation-id", "fixture-conversation", "--ref-msg-id", "message-1", "--ref-sender", "D-sender", "--text", "hello fixture", "--yes"}, "chat message search-advanced": {"chat", "message", "search-advanced", "--conversation-ids", "fixture-conversation", "--query", "fixture"}, - "chat message send": {"chat", "message", "send", "--user", "D-recipient", "--text", "hello fixture", "--uuid", "param-alias-equivalence", "--yes"}, + "chat message send": {"chat", "message", "send", "--user", "D-recipient", "--text", "hello fixture", "--idempotency-key", "param-alias-equivalence", "--yes"}, "chat message send-by-bot": {"chat", "message", "send-by-bot", "--robot-code", "robot-1", "--group", "fixture-conversation", "--title", "Fixture Alert", "--text", "@user-1 @user-2 fixture", "--at-user-ids", "user-1,user-2", "--yes"}, "chat message send-by-webhook": {"chat", "message", "send-by-webhook", "--token", "fixture-token", "--title", "Fixture Alert", "--text", "fixture", "--at-users", "user-1,user-2", "--yes"}, "contact +dept-members": {"contact", "+dept-members", "--dept", "Fixture Dept"}, @@ -163,8 +163,8 @@ var paramAliasCompleteCommandVariants = map[string]map[string][]string{ "sender-open-dingtalk-id": {"chat", "message", "list-by-sender", "--sender-open-dingtalk-id", "D-sender", "--start", "2026-03-10T00:00:00+08:00", "--end", "2026-03-11T00:00:00+08:00", "--limit", "7", "--cursor", "0"}, }, "chat message send": { - "group": {"chat", "message", "send", "--group", "fixture-conversation", "--text", "hello fixture", "--uuid", "param-alias-equivalence-group", "--yes"}, - "file-path": {"chat", "message", "send", "--group", "fixture-conversation", "--msg-type", "file", "--file-path", "../../go.mod", "--dentry-id", "1", "--space-id", "2", "--uuid", "param-alias-equivalence-file", "--yes"}, + "group": {"chat", "message", "send", "--group", "fixture-conversation", "--text", "hello fixture", "--idempotency-key", "param-alias-equivalence-group", "--yes"}, + "file-path": {"chat", "message", "send", "--group", "fixture-conversation", "--msg-type", "file", "--file-path", "../../go.mod", "--dentry-id", "1", "--space-id", "2", "--idempotency-key", "param-alias-equivalence-file", "--yes"}, }, "chat +conversation-set-top": { "conversation-ids": {"chat", "+conversation-set-top", "--conversation-ids", "fixture-conversation-1,fixture-conversation-2", "--yes"}, diff --git a/internal/app/root_help_test.go b/internal/app/root_help_test.go index eb370a7b..79c1c967 100644 --- a/internal/app/root_help_test.go +++ b/internal/app/root_help_test.go @@ -369,6 +369,14 @@ func TestChatFileUploadDownlinedButMessageFileSendStays(t *testing.T) { t.Fatalf("chat message send missing --%s", flag) } } + idempotencyKey := send.Flags().Lookup("idempotency-key") + if idempotencyKey == nil { + t.Fatal("chat message send missing --idempotency-key") + } + legacyUUID := send.Flags().Lookup("uuid") + if legacyUUID == nil || !legacyUUID.Hidden { + t.Fatalf("chat message send --uuid hidden = %#v, want hidden compatibility flag", legacyUUID) + } got, err := executeRootCaptureStdout(t, []string{ "chat", "file", "upload", diff --git a/internal/cli/schema_catalog_test.go b/internal/cli/schema_catalog_test.go index e653256a..0360ae3a 100644 --- a/internal/cli/schema_catalog_test.go +++ b/internal/cli/schema_catalog_test.go @@ -1356,6 +1356,7 @@ func TestDeliveryCatalogChatParamDeclsFrom87910880Reviewed(t *testing.T) { {"chat message edit", "conversation-id", "openConversationId", true, ""}, {"chat message edit", "msg-id", "openMessageId", true, ""}, {"chat message edit", "at-open-dingtalk-ids", "atOpenDingTalkIds", false, "array"}, + {"chat message send", "idempotency-key", "uuid", false, ""}, {"chat message send-card", "at-all", "atAll", false, ""}, {"chat message send-card", "at-open-dingtalk-ids", "atOpenDingTalkIds", false, "array"}, {"chat message update-text-emotion", "msg-id", "openMsgId", true, ""}, @@ -1407,6 +1408,14 @@ func TestDeliveryCatalogChatParamDeclsFrom87910880Reviewed(t *testing.T) { t.Fatalf("chat message edit unexpectedly publishes hidden alias --%s", hidden) } } + sendLeaf, err = queryDeliverySchemaPayload([]string{"chat message send"}) + if err != nil { + t.Fatal(err) + } + sendParams = schemaMap(sendLeaf["parameters"]) + if _, ok := sendParams["uuid"]; ok { + t.Fatal("chat message send unexpectedly publishes hidden alias --uuid") + } listByConv, err := queryDeliverySchemaPayload([]string{"chat category list-by-conv"}) if err != nil { t.Fatal(err) diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index dd91e2eb..1f8ad37f 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -2835,7 +2835,7 @@ func newChatCommand() *cobra.Command { groupID := flagOrFallback(cmd, "group", "conversation-id", "id", "chat") userID, _ := cmd.Flags().GetString("user") openDingTalkID, _ := cmd.Flags().GetString("open-dingtalk-id") - msgUuid, _ := cmd.Flags().GetString("uuid") + msgUuid := flagOrFallback(cmd, "idempotency-key", "uuid") specified := 0 if groupID != "" { specified++ @@ -3050,6 +3050,7 @@ func newChatCommand() *cobra.Command { {Name: "ai-tag", Property: "clawType", InterfaceType: "string"}, {Name: "at-open-dingtalk-ids", Property: "atOpenDingTalkIds"}, {Name: "group", Property: "openConversationId"}, + {Name: "idempotency-key", Property: "uuid"}, {Name: "open-dingtalk-id", Property: "receiverOpenDingTalkId"}, }, }, @@ -4385,7 +4386,9 @@ chat message edit 或 chat message recall 的 --msg-id 和 --conversation-id。 _ = chatMessageSendCmd.Flags().MarkHidden("file-type") _ = chatMessageSendCmd.Flags().MarkHidden("file-size") chatMessageSendCmd.Flags().Bool("ai-tag", true, "消息是否带 AI 发送角标(默认 true)") - chatMessageSendCmd.Flags().String("uuid", "", "幂等 UUID,相同 uuid 在 24h 内不会重复发送(可选)") + chatMessageSendCmd.Flags().String("idempotency-key", "", "幂等键,相同 key 在 24h 内不会重复发送(可选)") + chatMessageSendCmd.Flags().String("uuid", "", "--idempotency-key 的兼容别名") + _ = chatMessageSendCmd.Flags().MarkHidden("uuid") cli.AttachRuntimeSchema(chatMessageSendCmd, "chat", "send_personal_message", "hardcoded:chat") cli.AnnotateRuntimeConstraints(chatMessageSendCmd, cli.RuntimeSchemaConstraints{ MutuallyExclusive: [][]string{{"group", "user", "open-dingtalk-id"}},