feat(upload): update media upload response to use Unix timestamp for created_at
This commit is contained in:
parent
e46ef82aac
commit
ac1cdf1aa8
3 changed files with 134 additions and 4 deletions
|
|
@ -136,6 +136,15 @@ type wsSendMsgBody struct {
|
||||||
ChatType uint32 `json:"chat_type,omitempty"`
|
ChatType uint32 `json:"chat_type,omitempty"`
|
||||||
MsgType string `json:"msgtype"`
|
MsgType string `json:"msgtype"`
|
||||||
Markdown *wsMarkdownContent `json:"markdown,omitempty"`
|
Markdown *wsMarkdownContent `json:"markdown,omitempty"`
|
||||||
|
Image *wsMediaIDContent `json:"image,omitempty"`
|
||||||
|
Voice *wsMediaIDContent `json:"voice,omitempty"`
|
||||||
|
Video *wsMediaIDContent `json:"video,omitempty"`
|
||||||
|
File *wsMediaIDContent `json:"file,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// wsMediaIDContent is used by aibot_send_msg for media types (image/voice/video/file).
|
||||||
|
type wsMediaIDContent struct {
|
||||||
|
MediaID string `json:"media_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// wsRespondMsgBody is the body for aibot_respond_msg / aibot_respond_welcome_msg.
|
// wsRespondMsgBody is the body for aibot_respond_msg / aibot_respond_welcome_msg.
|
||||||
|
|
@ -359,6 +368,97 @@ func (c *WeComAIBotWSChannel) Send(ctx context.Context, msg bus.OutboundMessage)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendMedia implements channels.MediaSender.
|
||||||
|
// Each media part is uploaded via the WS long-connection upload API and then
|
||||||
|
// delivered as a proactive message push (aibot_send_msg) to the originating chat.
|
||||||
|
func (c *WeComAIBotWSChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMessage) error {
|
||||||
|
if !c.IsRunning() {
|
||||||
|
return channels.ErrNotRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
store := c.GetMediaStore()
|
||||||
|
if store == nil {
|
||||||
|
return fmt.Errorf("no media store available: %w", channels.ErrSendFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// msg.ChatID is the req_id set by dispatchWSAgentTask.
|
||||||
|
// Resolve the real WeCom chatID and chat_type from the req state.
|
||||||
|
// If no state is found (e.g. cron/scheduler push), treat msg.ChatID as the
|
||||||
|
// real chatID directly (chat_type defaults to 0 = single chat).
|
||||||
|
var realChatID string
|
||||||
|
var chatType uint32
|
||||||
|
_, route, ok := c.getReqState(msg.ChatID)
|
||||||
|
if ok {
|
||||||
|
realChatID = route.ChatID
|
||||||
|
chatType = route.ChatType
|
||||||
|
} else {
|
||||||
|
realChatID = msg.ChatID
|
||||||
|
}
|
||||||
|
|
||||||
|
if realChatID == "" {
|
||||||
|
return fmt.Errorf("chat ID is empty: %w", channels.ErrSendFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, part := range msg.Parts {
|
||||||
|
localPath, err := store.Resolve(part.Ref)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorCF("wecom_aibot", "Failed to resolve media ref", map[string]any{
|
||||||
|
"ref": part.Ref,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
continue // skip this part, try others
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map bus part type to WeCom media type.
|
||||||
|
var wsMediaType string
|
||||||
|
switch part.Type {
|
||||||
|
case "image":
|
||||||
|
wsMediaType = "image"
|
||||||
|
case "audio":
|
||||||
|
wsMediaType = "voice"
|
||||||
|
case "video":
|
||||||
|
wsMediaType = "video"
|
||||||
|
default:
|
||||||
|
wsMediaType = "file"
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := part.Filename
|
||||||
|
if filename == "" {
|
||||||
|
filename = filepath.Base(localPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaID, err := c.UploadWSMedia(ctx, localPath, filename, wsMediaType)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorCF("wecom_aibot", "Failed to upload media", map[string]any{
|
||||||
|
"type": wsMediaType,
|
||||||
|
"path": localPath,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
// Fallback: send caption as text if upload fails.
|
||||||
|
if part.Caption != "" {
|
||||||
|
_ = c.wsSendActivePush(realChatID, chatType, part.Caption)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.wsSendActivePushMedia(realChatID, chatType, wsMediaType, mediaID); err != nil {
|
||||||
|
logger.ErrorCF("wecom_aibot", "Failed to send media message", map[string]any{
|
||||||
|
"type": wsMediaType,
|
||||||
|
"media_id": mediaID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return fmt.Errorf("websocket media delivery failed: %w", channels.ErrSendFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.InfoCF("wecom_aibot", "Media sent via proactive push", map[string]any{
|
||||||
|
"type": wsMediaType,
|
||||||
|
"filename": filename,
|
||||||
|
"chat_id": realChatID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Connection management ----
|
// ---- Connection management ----
|
||||||
|
|
||||||
// wsBackoffResetDuration is the minimum duration a WebSocket connection must
|
// wsBackoffResetDuration is the minimum duration a WebSocket connection must
|
||||||
|
|
@ -1046,6 +1146,36 @@ func (c *WeComAIBotWSChannel) wsSendActivePush(chatID string, chatType uint32, c
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wsSendActivePushMedia sends a single media asset (identified by media_id) as a
|
||||||
|
// proactive push message using aibot_send_msg.
|
||||||
|
// wsMediaType must be one of "image", "voice", "video", "file".
|
||||||
|
func (c *WeComAIBotWSChannel) wsSendActivePushMedia(chatID string, chatType uint32, wsMediaType, mediaID string) error {
|
||||||
|
if chatID == "" {
|
||||||
|
return fmt.Errorf("chatid is empty")
|
||||||
|
}
|
||||||
|
body := wsSendMsgBody{
|
||||||
|
ChatID: chatID,
|
||||||
|
ChatType: chatType,
|
||||||
|
MsgType: wsMediaType,
|
||||||
|
}
|
||||||
|
mc := &wsMediaIDContent{MediaID: mediaID}
|
||||||
|
switch wsMediaType {
|
||||||
|
case "image":
|
||||||
|
body.Image = mc
|
||||||
|
case "voice":
|
||||||
|
body.Voice = mc
|
||||||
|
case "video":
|
||||||
|
body.Video = mc
|
||||||
|
default: // "file"
|
||||||
|
body.File = mc
|
||||||
|
}
|
||||||
|
return c.writeWSAndWait(wsCommand{
|
||||||
|
Cmd: "aibot_send_msg",
|
||||||
|
Headers: wsHeaders{ReqID: wsGenerateID()},
|
||||||
|
Body: body,
|
||||||
|
}, wsSendMsgTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
// writeWSAndWait writes cmd to the active connection and validates the command response.
|
// writeWSAndWait writes cmd to the active connection and validates the command response.
|
||||||
// It is a thin wrapper around callWSCommand that discards the response body.
|
// It is a thin wrapper around callWSCommand that discards the response body.
|
||||||
func (c *WeComAIBotWSChannel) writeWSAndWait(cmd wsCommand, timeout time.Duration) error {
|
func (c *WeComAIBotWSChannel) writeWSAndWait(cmd wsCommand, timeout time.Duration) error {
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ type wsUploadFinishBody struct {
|
||||||
type wsUploadFinishResponse struct {
|
type wsUploadFinishResponse struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
MediaID string `json:"media_id"`
|
MediaID string `json:"media_id"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt int64 `json:"created_at"` // Unix timestamp (seconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Public API ----
|
// ---- Public API ----
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,7 @@ func TestWsUploadChunkBodyBase64(t *testing.T) {
|
||||||
// TestWsUploadFinishResponseJSON checks that the finish response unmarshals
|
// TestWsUploadFinishResponseJSON checks that the finish response unmarshals
|
||||||
// the fields WeCom returns.
|
// the fields WeCom returns.
|
||||||
func TestWsUploadFinishResponseJSON(t *testing.T) {
|
func TestWsUploadFinishResponseJSON(t *testing.T) {
|
||||||
raw := `{"type":"file","media_id":"MEDIAID_ABCDE","created_at":"1680000000"}`
|
raw := `{"type":"file","media_id":"MEDIAID_ABCDE","created_at":1680000000}`
|
||||||
var resp wsUploadFinishResponse
|
var resp wsUploadFinishResponse
|
||||||
if err := json.Unmarshal([]byte(raw), &resp); err != nil {
|
if err := json.Unmarshal([]byte(raw), &resp); err != nil {
|
||||||
t.Fatalf("unmarshal: %v", err)
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
|
@ -248,8 +248,8 @@ func TestWsUploadFinishResponseJSON(t *testing.T) {
|
||||||
if resp.Type != "file" {
|
if resp.Type != "file" {
|
||||||
t.Errorf("type: got %q, want %q", resp.Type, "file")
|
t.Errorf("type: got %q, want %q", resp.Type, "file")
|
||||||
}
|
}
|
||||||
if resp.CreatedAt != "1680000000" {
|
if resp.CreatedAt != 1680000000 {
|
||||||
t.Errorf("created_at: got %q, want %q", resp.CreatedAt, "1680000000")
|
t.Errorf("created_at: got %d, want %d", resp.CreatedAt, int64(1680000000))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue