Repository navigation
fix(dingtalk): retry transient emotion failures #7329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -88,6 +88,8 @@ const ACK_REACTION_NAME = '👀'; | |||||||||||||||||||||||||||||||||||
| const ACK_EMOTION_ID = '2659900'; | ||||||||||||||||||||||||||||||||||||
| const ACK_EMOTION_BG_ID = 'im_bg_1'; | ||||||||||||||||||||||||||||||||||||
| const EMOTION_API = 'https://api.dingtalk.com/v1.0/robot/emotion'; | ||||||||||||||||||||||||||||||||||||
| const EMOTION_MAX_ATTEMPTS = 3; | ||||||||||||||||||||||||||||||||||||
| const EMOTION_RETRY_BASE_DELAY_MS = 250; | ||||||||||||||||||||||||||||||||||||
| const GROUP_MSG_API = 'https://api.dingtalk.com/v1.0/robot/groupMessages/send'; | ||||||||||||||||||||||||||||||||||||
| const DIRECT_MSG_API = | ||||||||||||||||||||||||||||||||||||
| 'https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend'; | ||||||||||||||||||||||||||||||||||||
|
|
@@ -677,31 +679,43 @@ export class DingtalkChannel extends ChannelBase { | |||||||||||||||||||||||||||||||||||
| ? await this.getProactiveToken() | ||||||||||||||||||||||||||||||||||||
| : this.getAccessToken(); | ||||||||||||||||||||||||||||||||||||
| if (!token) return; | ||||||||||||||||||||||||||||||||||||
| const resp = await fetch(`${EMOTION_API}/${endpoint}`, { | ||||||||||||||||||||||||||||||||||||
| method: 'POST', | ||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||
| 'x-acs-dingtalk-access-token': token, | ||||||||||||||||||||||||||||||||||||
| 'Content-Type': 'application/json', | ||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||
| body: JSON.stringify({ | ||||||||||||||||||||||||||||||||||||
| robotCode, | ||||||||||||||||||||||||||||||||||||
| openMsgId: msgId, | ||||||||||||||||||||||||||||||||||||
| openConversationId: conversationId, | ||||||||||||||||||||||||||||||||||||
| emotionType: 2, | ||||||||||||||||||||||||||||||||||||
| emotionName: ACK_REACTION_NAME, | ||||||||||||||||||||||||||||||||||||
| textEmotion: { | ||||||||||||||||||||||||||||||||||||
| emotionId: ACK_EMOTION_ID, | ||||||||||||||||||||||||||||||||||||
| emotionName: ACK_REACTION_NAME, | ||||||||||||||||||||||||||||||||||||
| text: ACK_REACTION_NAME, | ||||||||||||||||||||||||||||||||||||
| backgroundId: ACK_EMOTION_BG_ID, | ||||||||||||||||||||||||||||||||||||
| for (let attempt = 0; attempt < EMOTION_MAX_ATTEMPTS; attempt++) { | ||||||||||||||||||||||||||||||||||||
| const resp = await fetch(`${EMOTION_API}/${endpoint}`, { | ||||||||||||||||||||||||||||||||||||
| method: 'POST', | ||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||
| 'x-acs-dingtalk-access-token': token, | ||||||||||||||||||||||||||||||||||||
| 'Content-Type': 'application/json', | ||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| if (!resp.ok) { | ||||||||||||||||||||||||||||||||||||
| body: JSON.stringify({ | ||||||||||||||||||||||||||||||||||||
| robotCode, | ||||||||||||||||||||||||||||||||||||
| openMsgId: msgId, | ||||||||||||||||||||||||||||||||||||
| openConversationId: conversationId, | ||||||||||||||||||||||||||||||||||||
| emotionType: 2, | ||||||||||||||||||||||||||||||||||||
| emotionName: ACK_REACTION_NAME, | ||||||||||||||||||||||||||||||||||||
| textEmotion: { | ||||||||||||||||||||||||||||||||||||
| emotionId: ACK_EMOTION_ID, | ||||||||||||||||||||||||||||||||||||
| emotionName: ACK_REACTION_NAME, | ||||||||||||||||||||||||||||||||||||
| text: ACK_REACTION_NAME, | ||||||||||||||||||||||||||||||||||||
| backgroundId: ACK_EMOTION_BG_ID, | ||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| if (resp.ok) return; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const isTransient = resp.status === 429 || resp.status >= 500; | ||||||||||||||||||||||||||||||||||||
| if (isTransient && attempt < EMOTION_MAX_ATTEMPTS - 1) { | ||||||||||||||||||||||||||||||||||||
| await resp.body?.cancel(); | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+705
to
+707
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] 429 responses are retried without honouring the server's
Suggested change
— qwen3.7-max via Qwen Code /review |
||||||||||||||||||||||||||||||||||||
| await new Promise((resolve) => | ||||||||||||||||||||||||||||||||||||
| setTimeout(resolve, EMOTION_RETRY_BASE_DELAY_MS * 2 ** attempt), | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+706
to
+710
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Intermediate retry attempts produce no log output — only the final failure is logged. — Concrete cost: at 3 AM, an oncall engineer sees
Suggested change
— qwen3.7-max via Qwen Code /review |
||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const detail = sanitizeLogText(await resp.text().catch(() => ''), 500); | ||||||||||||||||||||||||||||||||||||
| process.stderr.write( | ||||||||||||||||||||||||||||||||||||
| `[DingTalk:${this.name}] emotion/${endpoint} failed: ${resp.status} ${detail}\n`, | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||
| // best-effort, don't break message flow | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: please add a focused 429 retry case. The new predicate has a distinct
status === 429branch, while the current recovery test only exercises 500 responses, so a future regression could silently drop the rate-limit behavior this PR promises. A short429 → 200case should be enough.