feat(wecom): add response timeout handling and improve WebSocket command acknowledgment
This commit is contained in:
parent
9073fd047b
commit
10c7835a1a
1 changed files with 35 additions and 25 deletions
|
|
@ -32,6 +32,7 @@ const (
|
||||||
wsConnectTimeout = 15 * time.Second
|
wsConnectTimeout = 15 * time.Second
|
||||||
wsSubscribeTimeout = 10 * time.Second
|
wsSubscribeTimeout = 10 * time.Second
|
||||||
wsSendMsgTimeout = 10 * time.Second
|
wsSendMsgTimeout = 10 * time.Second
|
||||||
|
wsRespondMsgTimeout = 10 * time.Second
|
||||||
wsMaxReconnectWait = 60 * time.Second
|
wsMaxReconnectWait = 60 * time.Second
|
||||||
wsInitialReconnect = time.Second
|
wsInitialReconnect = time.Second
|
||||||
|
|
||||||
|
|
@ -82,7 +83,6 @@ type wsTask struct {
|
||||||
StreamID string // our generated stream.id
|
StreamID string // our generated stream.id
|
||||||
CreatedTime time.Time
|
CreatedTime time.Time
|
||||||
answerCh chan string // agent delivers its reply here via Send()
|
answerCh chan string // agent delivers its reply here via Send()
|
||||||
mediaCh chan []bus.MediaPart // agent's media attachments (buffered: 1)
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
@ -738,7 +738,6 @@ func (c *WeComAIBotWSChannel) dispatchWSAgentTask(
|
||||||
StreamID: streamID,
|
StreamID: streamID,
|
||||||
CreatedTime: time.Now(),
|
CreatedTime: time.Now(),
|
||||||
answerCh: make(chan string, 1),
|
answerCh: make(chan string, 1),
|
||||||
mediaCh: make(chan []bus.MediaPart, 1),
|
|
||||||
ctx: taskCtx,
|
ctx: taskCtx,
|
||||||
cancel: taskCancel,
|
cancel: taskCancel,
|
||||||
}
|
}
|
||||||
|
|
@ -817,10 +816,10 @@ func (c *WeComAIBotWSChannel) dispatchWSAgentTask(
|
||||||
map[string]any{"chat_id": actualChatID, "tick": tickCount})
|
map[string]any{"chat_id": actualChatID, "tick": tickCount})
|
||||||
c.wsSendStreamChunk(reqID, streamID, false, hint)
|
c.wsSendStreamChunk(reqID, streamID, false, hint)
|
||||||
case <-deadlineTimer.C:
|
case <-deadlineTimer.C:
|
||||||
logger.WarnCF("wecom_aibot", "Stream deadline reached without agent reply",
|
logger.WarnCF("wecom_aibot", "Stream response deadline reached, waiting for agent to finish",
|
||||||
map[string]any{"chat_id": actualChatID, "stream_id": streamID})
|
map[string]any{"chat_id": actualChatID})
|
||||||
c.wsSendStreamFinish(reqID, streamID,
|
c.wsSendStreamChunk(reqID, streamID, false,
|
||||||
"⏳ Processing is taking longer than expected. Please resend your message to try again.")
|
"⏳ Processing is taking longer than expected, the response will be sent as soon as it's ready!")
|
||||||
return
|
return
|
||||||
case <-taskCtx.Done():
|
case <-taskCtx.Done():
|
||||||
// Give a short grace period so that a response queued in the bus
|
// Give a short grace period so that a response queued in the bus
|
||||||
|
|
@ -869,7 +868,7 @@ func (c *WeComAIBotWSChannel) wsSendStreamChunk(reqID, streamID string, finish b
|
||||||
"finish": finish,
|
"finish": finish,
|
||||||
"preview": utils.Truncate(content, 100),
|
"preview": utils.Truncate(content, 100),
|
||||||
})
|
})
|
||||||
c.writeWS(wsCommand{
|
cmd := wsCommand{
|
||||||
Cmd: "aibot_respond_msg",
|
Cmd: "aibot_respond_msg",
|
||||||
Headers: wsHeaders{ReqID: reqID},
|
Headers: wsHeaders{ReqID: reqID},
|
||||||
Body: wsRespondMsgBody{
|
Body: wsRespondMsgBody{
|
||||||
|
|
@ -880,8 +879,16 @@ func (c *WeComAIBotWSChannel) wsSendStreamChunk(reqID, streamID string, finish b
|
||||||
Content: content,
|
Content: content,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
if err := c.writeWSAndWait(cmd, wsRespondMsgTimeout); err != nil {
|
||||||
|
logger.WarnCF("wecom_aibot", "Stream chunk ack failed", map[string]any{
|
||||||
|
"req_id": reqID,
|
||||||
|
"stream_id": streamID,
|
||||||
|
"finish": finish,
|
||||||
|
"error": err,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// wsSendStreamFinish sends the final aibot_respond_msg frame (finish=true, no images).
|
// wsSendStreamFinish sends the final aibot_respond_msg frame (finish=true, no images).
|
||||||
func (c *WeComAIBotWSChannel) wsSendStreamFinish(reqID, streamID, content string) {
|
func (c *WeComAIBotWSChannel) wsSendStreamFinish(reqID, streamID, content string) {
|
||||||
|
|
@ -891,14 +898,18 @@ func (c *WeComAIBotWSChannel) wsSendStreamFinish(reqID, streamID, content string
|
||||||
// wsSendWelcomeMsg sends a text welcome message via aibot_respond_welcome_msg.
|
// wsSendWelcomeMsg sends a text welcome message via aibot_respond_welcome_msg.
|
||||||
func (c *WeComAIBotWSChannel) wsSendWelcomeMsg(reqID, content string) {
|
func (c *WeComAIBotWSChannel) wsSendWelcomeMsg(reqID, content string) {
|
||||||
logger.DebugCF("wecom_aibot", "Sending welcome message", map[string]any{"req_id": reqID})
|
logger.DebugCF("wecom_aibot", "Sending welcome message", map[string]any{"req_id": reqID})
|
||||||
c.writeWS(wsCommand{
|
cmd := wsCommand{
|
||||||
Cmd: "aibot_respond_welcome_msg",
|
Cmd: "aibot_respond_welcome_msg",
|
||||||
Headers: wsHeaders{ReqID: reqID},
|
Headers: wsHeaders{ReqID: reqID},
|
||||||
Body: wsRespondMsgBody{
|
Body: wsRespondMsgBody{
|
||||||
MsgType: "text",
|
MsgType: "text",
|
||||||
Text: &wsTextContent{Content: content},
|
Text: &wsTextContent{Content: content},
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
if err := c.writeWSAndWait(cmd, wsRespondMsgTimeout); err != nil {
|
||||||
|
logger.WarnCF("wecom_aibot", "Welcome message ack failed",
|
||||||
|
map[string]any{"req_id": reqID, "error": err})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// wsSendActivePush sends a proactive markdown message using aibot_send_msg.
|
// wsSendActivePush sends a proactive markdown message using aibot_send_msg.
|
||||||
|
|
@ -935,28 +946,27 @@ func (c *WeComAIBotWSChannel) wsSendActivePush(chatID string, chatType uint32, c
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeWS serializes cmd to JSON and writes it to the active WebSocket
|
// writeWSAndWait writes cmd to the active connection and validates the command response.
|
||||||
// connection. It is safe to call from multiple goroutines.
|
func (c *WeComAIBotWSChannel) writeWSAndWait(cmd wsCommand, timeout time.Duration) error {
|
||||||
func (c *WeComAIBotWSChannel) writeWS(cmd any) {
|
if cmd.Headers.ReqID == "" {
|
||||||
data, err := json.Marshal(cmd)
|
return fmt.Errorf("req_id is empty")
|
||||||
if err != nil {
|
|
||||||
logger.ErrorCF("wecom_aibot", "Failed to marshal WebSocket command",
|
|
||||||
map[string]any{"error": err})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.connMu.Lock()
|
c.connMu.Lock()
|
||||||
conn := c.conn
|
conn := c.conn
|
||||||
if conn != nil {
|
|
||||||
err = conn.WriteMessage(websocket.TextMessage, data)
|
|
||||||
}
|
|
||||||
c.connMu.Unlock()
|
c.connMu.Unlock()
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
logger.WarnC("wecom_aibot", "WebSocket connection unavailable, dropping outbound message")
|
return fmt.Errorf("websocket not connected")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resp, err := c.sendAndWait(conn, cmd.Headers.ReqID, cmd, timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WarnCF("wecom_aibot", "WebSocket write failed", map[string]any{"error": err})
|
return err
|
||||||
}
|
}
|
||||||
|
if resp.ErrCode != 0 {
|
||||||
|
return fmt.Errorf("%s rejected (errcode=%d): %s", cmd.Cmd, resp.ErrCode, resp.ErrMsg)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// cancelAllTasks cancels every pending agent task; called when the connection drops.
|
// cancelAllTasks cancels every pending agent task; called when the connection drops.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue