feat(upload): update media upload response to use Unix timestamp for created_at

This commit is contained in:
Zhang Rui 2026-03-19 22:30:23 +08:00
parent e46ef82aac
commit ac1cdf1aa8
3 changed files with 134 additions and 4 deletions

View file

@ -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 {

View file

@ -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 ----

View file

@ -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))
} }
} }