From 256b4bf624aa7256b54a20d953d9ec3d7b196e89 Mon Sep 17 00:00:00 2001 From: alick-liming Date: Fri, 4 Sep 2026 10:27:27 +0800 Subject: [PATCH 1/4] feat: support json v2 ephemeral cards --- api_message_send_ephemeral.go | 12 +++---- api_message_send_ephemeral_test.go | 51 ++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 api_message_send_ephemeral_test.go diff --git a/api_message_send_ephemeral.go b/api_message_send_ephemeral.go index 3985e71c..f12faf31 100644 --- a/api_message_send_ephemeral.go +++ b/api_message_send_ephemeral.go @@ -69,12 +69,12 @@ func (r *Mock) UnMockMessageSendEphemeralMessage() { // SendEphemeralMessageReq ... type SendEphemeralMessageReq struct { - ChatID string `json:"chat_id,omitempty"` // 发送临时消息的群ID可通过事件推送获取 - OpenID string `json:"open_id,omitempty"` // 指定发送临时消息卡片的用户, 其他人将无法看到临时消息卡片;只需要填 open_id、email、user_id中的一个即可, 推荐使用 OpenID, 获取方式可参考文档[如何获取 Open ID?](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-openid) (服务端依次读取字段的顺序为 open_id > user_id > email) - UserID string `json:"user_id,omitempty"` // 指定发送临时消息卡片的用户, 其他人将无法看到临时消息卡片;只需要填 open_id、email、user_id中的一个即可, 推荐使用 OpenID, 获取方式可参考文档[如何获取 Open ID?](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-openid) (服务端依次读取字段的顺序为 open_id > user_id > email) - Email string `json:"email,omitempty"` // 指定发送临时消息卡片的用户, 其他人将无法看到临时消息卡片;只需要填 open_id、email、user_id中的一个即可, 推荐使用 OpenID, 获取方式可参考文档[如何获取 Open ID?](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-openid) (服务端依次读取字段的顺序为 open_id > user_id > email) - MsgType MsgType `json:"msg_type,omitempty"` // 消息的类型, 此处固定填 "interactive" - Card *MessageContentCard `json:"card,omitempty"` // 消息卡片的描述内容, 具体参考 [基础结构](https://open.feishu.cn/document/ukTMukTMukTM/uEjNwUjLxYDM14SM2ATN) + ChatID string `json:"chat_id,omitempty"` // 发送临时消息的群ID可通过事件推送获取 + OpenID string `json:"open_id,omitempty"` // 指定发送临时消息卡片的用户, 其他人将无法看到临时消息卡片;只需要填 open_id、email、user_id中的一个即可, 推荐使用 OpenID, 获取方式可参考文档[如何获取 Open ID?](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-openid) (服务端依次读取字段的顺序为 open_id > user_id > email) + UserID string `json:"user_id,omitempty"` // 指定发送临时消息卡片的用户, 其他人将无法看到临时消息卡片;只需要填 open_id、email、user_id中的一个即可, 推荐使用 OpenID, 获取方式可参考文档[如何获取 Open ID?](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-openid) (服务端依次读取字段的顺序为 open_id > user_id > email) + Email string `json:"email,omitempty"` // 指定发送临时消息卡片的用户, 其他人将无法看到临时消息卡片;只需要填 open_id、email、user_id中的一个即可, 推荐使用 OpenID, 获取方式可参考文档[如何获取 Open ID?](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-openid) (服务端依次读取字段的顺序为 open_id > user_id > email) + MsgType MsgType `json:"msg_type,omitempty"` // 消息的类型, 此处固定填 "interactive" + Card any `json:"card,omitempty"` // 消息卡片内容,支持 JSON 1.0 和 JSON 2.0 } // SendEphemeralMessageResp ... diff --git a/api_message_send_ephemeral_test.go b/api_message_send_ephemeral_test.go new file mode 100644 index 00000000..9c88ff91 --- /dev/null +++ b/api_message_send_ephemeral_test.go @@ -0,0 +1,51 @@ +package lark + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSendEphemeralMessageReqMarshalsV2Card(t *testing.T) { + card := json.RawMessage(`{"schema":"2.0","body":{"elements":[]}}`) + req := &SendEphemeralMessageReq{ + ChatID: "oc_test", + OpenID: "ou_test", + MsgType: MsgTypeInteractive, + Card: card, + } + + body, err := json.Marshal(req) + require.NoError(t, err) + require.JSONEq(t, `{ + "chat_id":"oc_test", + "open_id":"ou_test", + "msg_type":"interactive", + "card":{"schema":"2.0","body":{"elements":[]}} + }`, string(body)) +} + +func TestSendEphemeralMessageReqKeepsV1CardCompatibility(t *testing.T) { + req := &SendEphemeralMessageReq{ + ChatID: "oc_test", + OpenID: "ou_test", + MsgType: MsgTypeInteractive, + Card: &MessageContentCard{ + Modules: []MessageContentCardModule{ + MessageContentCardModuleDIV{ + Text: &MessageContentCardObjectText{Tag: "lark_md", Content: "hello"}, + }, + }, + }, + } + + body, err := json.Marshal(req) + require.NoError(t, err) + require.JSONEq(t, `{ + "chat_id":"oc_test", + "open_id":"ou_test", + "msg_type":"interactive", + "card":{"elements":[{"tag":"div","text":{"tag":"lark_md","content":"hello"}}]} + }`, string(body)) +} From 0d3f70d665c6065be2be1ecabb63303c432f4254 Mon Sep 17 00:00:00 2001 From: alick-liming Date: Fri, 4 Sep 2026 11:00:52 +0800 Subject: [PATCH 2/4] fix: preserve v1 ephemeral card typing --- api_message_send_ephemeral.go | 14 ++++++++------ api_message_send_ephemeral_json.go | 28 ++++++++++++++++++++++++++++ api_message_send_ephemeral_test.go | 29 +++++++++++++++++++++-------- 3 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 api_message_send_ephemeral_json.go diff --git a/api_message_send_ephemeral.go b/api_message_send_ephemeral.go index f12faf31..c6848f98 100644 --- a/api_message_send_ephemeral.go +++ b/api_message_send_ephemeral.go @@ -19,6 +19,7 @@ package lark import ( "context" + "encoding/json" ) // SendEphemeralMessage 用于机器人在群会话中发送仅指定用户可见的消息卡片。卡片上将展示"仅对你可见"标识。 @@ -69,12 +70,13 @@ func (r *Mock) UnMockMessageSendEphemeralMessage() { // SendEphemeralMessageReq ... type SendEphemeralMessageReq struct { - ChatID string `json:"chat_id,omitempty"` // 发送临时消息的群ID可通过事件推送获取 - OpenID string `json:"open_id,omitempty"` // 指定发送临时消息卡片的用户, 其他人将无法看到临时消息卡片;只需要填 open_id、email、user_id中的一个即可, 推荐使用 OpenID, 获取方式可参考文档[如何获取 Open ID?](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-openid) (服务端依次读取字段的顺序为 open_id > user_id > email) - UserID string `json:"user_id,omitempty"` // 指定发送临时消息卡片的用户, 其他人将无法看到临时消息卡片;只需要填 open_id、email、user_id中的一个即可, 推荐使用 OpenID, 获取方式可参考文档[如何获取 Open ID?](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-openid) (服务端依次读取字段的顺序为 open_id > user_id > email) - Email string `json:"email,omitempty"` // 指定发送临时消息卡片的用户, 其他人将无法看到临时消息卡片;只需要填 open_id、email、user_id中的一个即可, 推荐使用 OpenID, 获取方式可参考文档[如何获取 Open ID?](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-openid) (服务端依次读取字段的顺序为 open_id > user_id > email) - MsgType MsgType `json:"msg_type,omitempty"` // 消息的类型, 此处固定填 "interactive" - Card any `json:"card,omitempty"` // 消息卡片内容,支持 JSON 1.0 和 JSON 2.0 + ChatID string `json:"chat_id,omitempty"` // 发送临时消息的群ID可通过事件推送获取 + OpenID string `json:"open_id,omitempty"` // 指定发送临时消息卡片的用户, 其他人将无法看到临时消息卡片;只需要填 open_id、email、user_id中的一个即可, 推荐使用 OpenID, 获取方式可参考文档[如何获取 Open ID?](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-openid) (服务端依次读取字段的顺序为 open_id > user_id > email) + UserID string `json:"user_id,omitempty"` // 指定发送临时消息卡片的用户, 其他人将无法看到临时消息卡片;只需要填 open_id、email、user_id中的一个即可, 推荐使用 OpenID, 获取方式可参考文档[如何获取 Open ID?](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-openid) (服务端依次读取字段的顺序为 open_id > user_id > email) + Email string `json:"email,omitempty"` // 指定发送临时消息卡片的用户, 其他人将无法看到临时消息卡片;只需要填 open_id、email、user_id中的一个即可, 推荐使用 OpenID, 获取方式可参考文档[如何获取 Open ID?](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-openid) (服务端依次读取字段的顺序为 open_id > user_id > email) + MsgType MsgType `json:"msg_type,omitempty"` // 消息的类型, 此处固定填 "interactive" + Card *MessageContentCard `json:"-"` // JSON 1.0 消息卡片内容 + CardV2 json.RawMessage `json:"-"` // JSON 2.0 消息卡片内容 } // SendEphemeralMessageResp ... diff --git a/api_message_send_ephemeral_json.go b/api_message_send_ephemeral_json.go new file mode 100644 index 00000000..d04cde28 --- /dev/null +++ b/api_message_send_ephemeral_json.go @@ -0,0 +1,28 @@ +package lark + +import ( + "encoding/json" + "errors" +) + +func (r SendEphemeralMessageReq) MarshalJSON() ([]byte, error) { + if r.Card != nil && len(r.CardV2) > 0 { + return nil, errors.New("lark: SendEphemeralMessageReq Card and CardV2 cannot both be set") + } + + var card any + if len(r.CardV2) > 0 { + card = r.CardV2 + } else if r.Card != nil { + card = r.Card + } + + type request SendEphemeralMessageReq + return json.Marshal(struct { + request + Card any `json:"card,omitempty"` + }{ + request: request(r), + Card: card, + }) +} diff --git a/api_message_send_ephemeral_test.go b/api_message_send_ephemeral_test.go index 9c88ff91..41f718af 100644 --- a/api_message_send_ephemeral_test.go +++ b/api_message_send_ephemeral_test.go @@ -13,7 +13,7 @@ func TestSendEphemeralMessageReqMarshalsV2Card(t *testing.T) { ChatID: "oc_test", OpenID: "ou_test", MsgType: MsgTypeInteractive, - Card: card, + CardV2: card, } body, err := json.Marshal(req) @@ -27,18 +27,21 @@ func TestSendEphemeralMessageReqMarshalsV2Card(t *testing.T) { } func TestSendEphemeralMessageReqKeepsV1CardCompatibility(t *testing.T) { + card := &MessageContentCard{ + Modules: []MessageContentCardModule{ + MessageContentCardModuleDIV{ + Text: &MessageContentCardObjectText{Tag: "lark_md", Content: "hello"}, + }, + }, + } req := &SendEphemeralMessageReq{ ChatID: "oc_test", OpenID: "ou_test", MsgType: MsgTypeInteractive, - Card: &MessageContentCard{ - Modules: []MessageContentCardModule{ - MessageContentCardModuleDIV{ - Text: &MessageContentCardObjectText{Tag: "lark_md", Content: "hello"}, - }, - }, - }, + Card: card, } + var typedCard *MessageContentCard = req.Card + require.Same(t, card, typedCard) body, err := json.Marshal(req) require.NoError(t, err) @@ -49,3 +52,13 @@ func TestSendEphemeralMessageReqKeepsV1CardCompatibility(t *testing.T) { "card":{"elements":[{"tag":"div","text":{"tag":"lark_md","content":"hello"}}]} }`, string(body)) } + +func TestSendEphemeralMessageReqRejectsBothCardVersions(t *testing.T) { + req := &SendEphemeralMessageReq{ + Card: &MessageContentCard{}, + CardV2: json.RawMessage(`{"schema":"2.0","body":{"elements":[]}}`), + } + + _, err := json.Marshal(req) + require.ErrorContains(t, err, "Card and CardV2 cannot both be set") +} From 54d9aac6f9e529b74081761671bbd6a4d34cb300 Mon Sep 17 00:00:00 2001 From: alick-liming Date: Fri, 4 Sep 2026 11:08:32 +0800 Subject: [PATCH 3/4] test: cover empty ephemeral card payload --- api_message_send_ephemeral_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/api_message_send_ephemeral_test.go b/api_message_send_ephemeral_test.go index 41f718af..2220d526 100644 --- a/api_message_send_ephemeral_test.go +++ b/api_message_send_ephemeral_test.go @@ -62,3 +62,19 @@ func TestSendEphemeralMessageReqRejectsBothCardVersions(t *testing.T) { _, err := json.Marshal(req) require.ErrorContains(t, err, "Card and CardV2 cannot both be set") } + +func TestSendEphemeralMessageReqOmitsEmptyCard(t *testing.T) { + req := &SendEphemeralMessageReq{ + ChatID: "oc_test", + OpenID: "ou_test", + MsgType: MsgTypeInteractive, + } + + body, err := json.Marshal(req) + require.NoError(t, err) + require.JSONEq(t, `{ + "chat_id":"oc_test", + "open_id":"ou_test", + "msg_type":"interactive" + }`, string(body)) +} From ff041e99c193224d4d7218e6fdf07e2a7b76c945 Mon Sep 17 00:00:00 2001 From: alick-liming Date: Mon, 7 Sep 2026 11:11:20 +0800 Subject: [PATCH 4/4] test: align request fixtures with current SDK types --- internal_test.go | 39 +++++++++++++++++++++++---------------- test/drive_file_test.go | 1 - 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/internal_test.go b/internal_test.go index 7fc65e7a..1164b811 100644 --- a/internal_test.go +++ b/internal_test.go @@ -42,8 +42,9 @@ func Test_Request(t *testing.T) { } resp, err := ins.parseRawHttpRequest(ctx, &RawRequestReq{ - Method: "get", - URL: "http://x.com/:id", + MethodOption: newMethodOption(nil), + Method: "get", + URL: "http://x.com/:id", Body: req{ ID: string("1234"), }, @@ -60,8 +61,9 @@ func Test_Request(t *testing.T) { ID int `path:"id"` } resp, err := ins.parseRawHttpRequest(ctx, &RawRequestReq{ - Method: "get", - URL: "http://x.com/:id", + MethodOption: newMethodOption(nil), + Method: "get", + URL: "http://x.com/:id", Body: req{ ID: 1234, }, @@ -79,8 +81,9 @@ func Test_Request(t *testing.T) { ID int `path:"id"` } resp, err := ins.parseRawHttpRequest(ctx, &RawRequestReq{ - Method: "get", - URL: "http://x.com/:type/:id", + MethodOption: newMethodOption(nil), + Method: "get", + URL: "http://x.com/:type/:id", Body: req{ Type: "x", ID: 1234, @@ -99,8 +102,9 @@ func Test_Request(t *testing.T) { ID int `path:"id"` } resp, err := ins.parseRawHttpRequest(ctx, &RawRequestReq{ - Method: "get", - URL: "http://x.com/:id", + MethodOption: newMethodOption(nil), + Method: "get", + URL: "http://x.com/:id", Body: req{ Type: "x", ID: 1234, @@ -120,8 +124,9 @@ func Test_Request(t *testing.T) { Name string `json:"name"` } resp, err := ins.parseRawHttpRequest(ctx, &RawRequestReq{ - Method: "get", - URL: "http://x.com/:id", + MethodOption: newMethodOption(nil), + Method: "get", + URL: "http://x.com/:id", Body: req{ Type: "x", ID: 1234, @@ -144,9 +149,10 @@ func Test_Request(t *testing.T) { Image io.Reader `json:"image,omitempty"` // 图片内容,**示例值**:二进流 } resp, err := ins.parseRawHttpRequest(ctx, &RawRequestReq{ - Method: "get", - URL: "http://x.com", - IsFile: true, + MethodOption: newMethodOption(nil), + Method: "get", + URL: "http://x.com", + IsFile: true, Body: req{ ImageType: ImageTypeMessage, Image: bytes.NewReader([]byte("hi")), @@ -167,9 +173,10 @@ func Test_Request(t *testing.T) { Types []string `query:"types"` } resp, err := ins.parseRawHttpRequest(ctx, &RawRequestReq{ - Method: "get", - URL: "http://x.com", - IsFile: true, + MethodOption: newMethodOption(nil), + Method: "get", + URL: "http://x.com", + IsFile: true, Body: req{ Types: []string{"a", "b"}, }, diff --git a/test/drive_file_test.go b/test/drive_file_test.go index 8a6987dc..5f17d2f7 100644 --- a/test/drive_file_test.go +++ b/test/drive_file_test.go @@ -63,7 +63,6 @@ func Test_DriveFile(t *testing.T) { { resp, _, err := AppAllPermission.Ins().Drive.DownloadDriveFile(ctx, &lark.DownloadDriveFileReq{ FileToken: fileToken, - Range: [2]int64{}, }) as.Nil(err) as.NotNil(resp)