159 lines
4.5 KiB
Go
159 lines
4.5 KiB
Go
package wecom
|
|
|
|
import (
|
|
"context"
|
|
"crypto/md5"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/chenhg5/cc-connect/core"
|
|
)
|
|
|
|
const (
|
|
wecomWSUploadChunkSize = 512 * 1024
|
|
wecomWSUploadMaxChunks = 100
|
|
)
|
|
|
|
// SendImage uploads and sends an image through the WeCom AI Bot WebSocket API.
|
|
func (p *WSPlatform) SendImage(ctx context.Context, rctx any, img core.ImageAttachment) error {
|
|
rc, ok := rctx.(wsReplyContext)
|
|
if !ok {
|
|
return fmt.Errorf("wecom-ws: SendImage: invalid reply context type %T", rctx)
|
|
}
|
|
if rc.chatID == "" {
|
|
return fmt.Errorf("wecom-ws: chatID is empty, cannot send image")
|
|
}
|
|
if len(img.Data) == 0 {
|
|
return fmt.Errorf("wecom-ws: image data is empty")
|
|
}
|
|
|
|
mediaID, err := p.uploadWSMedia(ctx, "image", wsImageFileName(img), img.Data)
|
|
if err != nil {
|
|
return fmt.Errorf("wecom-ws: send image: %w", err)
|
|
}
|
|
if err := p.sendWSMediaMessage(ctx, rc.chatID, "image", mediaID); err != nil {
|
|
return fmt.Errorf("wecom-ws: send image: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *WSPlatform) uploadWSMedia(ctx context.Context, mediaType, filename string, data []byte) (string, error) {
|
|
totalChunks := (len(data) + wecomWSUploadChunkSize - 1) / wecomWSUploadChunkSize
|
|
if totalChunks == 0 {
|
|
return "", fmt.Errorf("empty media data")
|
|
}
|
|
if totalChunks > wecomWSUploadMaxChunks {
|
|
return "", fmt.Errorf("media too large: %d chunks exceeds maximum %d", totalChunks, wecomWSUploadMaxChunks)
|
|
}
|
|
|
|
sum := md5.Sum(data)
|
|
initReqID := p.generateReqID("aibot_upload_media_init")
|
|
initFrame := map[string]any{
|
|
"cmd": "aibot_upload_media_init",
|
|
"headers": map[string]string{"req_id": initReqID},
|
|
"body": map[string]any{
|
|
"type": mediaType,
|
|
"filename": filename,
|
|
"total_size": len(data),
|
|
"total_chunks": totalChunks,
|
|
"md5": hex.EncodeToString(sum[:]),
|
|
},
|
|
}
|
|
initResp, err := p.writeAndWaitFrameWithTimeout(ctx, initFrame, initReqID, wsMediaAckTimeout)
|
|
if err != nil {
|
|
return "", fmt.Errorf("upload init: %w", err)
|
|
}
|
|
var initBody struct {
|
|
UploadID string `json:"upload_id"`
|
|
}
|
|
if err := json.Unmarshal(initResp.Body, &initBody); err != nil {
|
|
return "", fmt.Errorf("decode upload init response: %w", err)
|
|
}
|
|
if initBody.UploadID == "" {
|
|
return "", fmt.Errorf("upload init: empty upload_id")
|
|
}
|
|
|
|
for i := 0; i < totalChunks; i++ {
|
|
start := i * wecomWSUploadChunkSize
|
|
end := start + wecomWSUploadChunkSize
|
|
if end > len(data) {
|
|
end = len(data)
|
|
}
|
|
reqID := p.generateReqID("aibot_upload_media_chunk")
|
|
chunkFrame := map[string]any{
|
|
"cmd": "aibot_upload_media_chunk",
|
|
"headers": map[string]string{"req_id": reqID},
|
|
"body": map[string]any{
|
|
"upload_id": initBody.UploadID,
|
|
"chunk_index": i,
|
|
"base64_data": base64.StdEncoding.EncodeToString(data[start:end]),
|
|
},
|
|
}
|
|
if _, err := p.writeAndWaitFrameWithTimeout(ctx, chunkFrame, reqID, wsMediaAckTimeout); err != nil {
|
|
return "", fmt.Errorf("upload chunk %d: %w", i, err)
|
|
}
|
|
}
|
|
|
|
finishReqID := p.generateReqID("aibot_upload_media_finish")
|
|
finishFrame := map[string]any{
|
|
"cmd": "aibot_upload_media_finish",
|
|
"headers": map[string]string{"req_id": finishReqID},
|
|
"body": map[string]any{
|
|
"upload_id": initBody.UploadID,
|
|
},
|
|
}
|
|
finishResp, err := p.writeAndWaitFrameWithTimeout(ctx, finishFrame, finishReqID, wsMediaAckTimeout)
|
|
if err != nil {
|
|
return "", fmt.Errorf("upload finish: %w", err)
|
|
}
|
|
var finishBody struct {
|
|
MediaID string `json:"media_id"`
|
|
}
|
|
if err := json.Unmarshal(finishResp.Body, &finishBody); err != nil {
|
|
return "", fmt.Errorf("decode upload finish response: %w", err)
|
|
}
|
|
if finishBody.MediaID == "" {
|
|
return "", fmt.Errorf("upload finish: empty media_id")
|
|
}
|
|
return finishBody.MediaID, nil
|
|
}
|
|
|
|
func (p *WSPlatform) sendWSMediaMessage(ctx context.Context, chatID, mediaType, mediaID string) error {
|
|
reqID := p.generateReqID("aibot_send_msg")
|
|
frame := map[string]any{
|
|
"cmd": "aibot_send_msg",
|
|
"headers": map[string]string{"req_id": reqID},
|
|
"body": map[string]any{
|
|
"chatid": chatID,
|
|
"msgtype": mediaType,
|
|
mediaType: map[string]string{
|
|
"media_id": mediaID,
|
|
},
|
|
},
|
|
}
|
|
return p.writeAndWaitAckStrict(ctx, frame, reqID, wsMediaAckTimeout)
|
|
}
|
|
|
|
func wsImageFileName(img core.ImageAttachment) string {
|
|
name := filepath.Base(strings.TrimSpace(img.FileName))
|
|
if name != "" && name != "." {
|
|
return name
|
|
}
|
|
switch strings.ToLower(img.MimeType) {
|
|
case "image/jpeg", "image/jpg":
|
|
return "image.jpg"
|
|
case "image/gif":
|
|
return "image.gif"
|
|
case "image/webp":
|
|
return "image.webp"
|
|
default:
|
|
return "image.png"
|
|
}
|
|
}
|
|
|
|
var _ core.ImageSender = (*WSPlatform)(nil)
|