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"`
|
||||
MsgType string `json:"msgtype"`
|
||||
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.
|
||||
|
|
@ -359,6 +368,97 @@ func (c *WeComAIBotWSChannel) Send(ctx context.Context, msg bus.OutboundMessage)
|
|||
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 ----
|
||||
|
||||
// wsBackoffResetDuration is the minimum duration a WebSocket connection must
|
||||
|
|
@ -1046,6 +1146,36 @@ func (c *WeComAIBotWSChannel) wsSendActivePush(chatID string, chatType uint32, c
|
|||
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.
|
||||
// It is a thin wrapper around callWSCommand that discards the response body.
|
||||
func (c *WeComAIBotWSChannel) writeWSAndWait(cmd wsCommand, timeout time.Duration) error {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ type wsUploadFinishBody struct {
|
|||
type wsUploadFinishResponse struct {
|
||||
Type string `json:"type"`
|
||||
MediaID string `json:"media_id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
CreatedAt int64 `json:"created_at"` // Unix timestamp (seconds)
|
||||
}
|
||||
|
||||
// ---- Public API ----
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ func TestWsUploadChunkBodyBase64(t *testing.T) {
|
|||
// TestWsUploadFinishResponseJSON checks that the finish response unmarshals
|
||||
// the fields WeCom returns.
|
||||
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
|
||||
if err := json.Unmarshal([]byte(raw), &resp); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
|
|
@ -248,8 +248,8 @@ func TestWsUploadFinishResponseJSON(t *testing.T) {
|
|||
if resp.Type != "file" {
|
||||
t.Errorf("type: got %q, want %q", resp.Type, "file")
|
||||
}
|
||||
if resp.CreatedAt != "1680000000" {
|
||||
t.Errorf("created_at: got %q, want %q", resp.CreatedAt, "1680000000")
|
||||
if resp.CreatedAt != 1680000000 {
|
||||
t.Errorf("created_at: got %d, want %d", resp.CreatedAt, int64(1680000000))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue