mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 11:41:04 +00:00
feat(channels): add reaction feedback and webhook caching for DingTalk
- Cache sessionWebhook by conversationId for reliable message routing
- Show 👀 reaction while processing messages, then recall it
- Use conversationId as chatId instead of webhook URL
- Fix rawData parsing for already-parsed message data
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
92c54ff309
commit
217964b849
1 changed files with 111 additions and 9 deletions
|
|
@ -14,10 +14,17 @@ import type {
|
|||
/** Track seen msgIds to deduplicate retried callbacks. */
|
||||
const DEDUP_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
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';
|
||||
|
||||
export class DingtalkChannel extends ChannelBase {
|
||||
private client: DWClient;
|
||||
private seenMessages: Map<string, number> = new Map();
|
||||
private dedupTimer?: ReturnType<typeof setInterval>;
|
||||
/** Map conversationId → latest sessionWebhook URL for sending replies. */
|
||||
private webhooks: Map<string, string> = new Map();
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
|
|
@ -68,7 +75,15 @@ export class DingtalkChannel extends ChannelBase {
|
|||
}
|
||||
|
||||
async sendMessage(chatId: string, text: string): Promise<void> {
|
||||
// chatId is the sessionWebhook URL for DingTalk
|
||||
// chatId is a conversationId — resolve to the latest sessionWebhook
|
||||
const webhook = this.webhooks.get(chatId);
|
||||
if (!webhook) {
|
||||
process.stderr.write(
|
||||
`[DingTalk:${this.name}] No webhook for chatId ${chatId}, cannot send.\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const body = {
|
||||
msgtype: 'markdown',
|
||||
markdown: {
|
||||
|
|
@ -77,7 +92,7 @@ export class DingtalkChannel extends ChannelBase {
|
|||
},
|
||||
};
|
||||
|
||||
const resp = await fetch(chatId, {
|
||||
const resp = await fetch(webhook, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
|
|
@ -91,6 +106,67 @@ export class DingtalkChannel extends ChannelBase {
|
|||
}
|
||||
}
|
||||
|
||||
private getAccessToken(): string | undefined {
|
||||
return this.client.getConfig().access_token;
|
||||
}
|
||||
|
||||
private async emotionApi(
|
||||
endpoint: 'reply' | 'recall',
|
||||
msgId: string,
|
||||
conversationId: string,
|
||||
): Promise<void> {
|
||||
const token = this.getAccessToken();
|
||||
if (!token) return;
|
||||
|
||||
const robotCode = this.config.clientId;
|
||||
if (!robotCode || !msgId || !conversationId) return;
|
||||
|
||||
try {
|
||||
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,
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const detail = await resp.text().catch(() => '');
|
||||
process.stderr.write(
|
||||
`[DingTalk:${this.name}] emotion/${endpoint} failed: ${resp.status} ${detail}\n`,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// best-effort, don't break message flow
|
||||
}
|
||||
}
|
||||
|
||||
private async attachReaction(
|
||||
msgId: string,
|
||||
conversationId: string,
|
||||
): Promise<void> {
|
||||
await this.emotionApi('reply', msgId, conversationId);
|
||||
}
|
||||
|
||||
private async recallReaction(
|
||||
msgId: string,
|
||||
conversationId: string,
|
||||
): Promise<void> {
|
||||
await this.emotionApi('recall', msgId, conversationId);
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
if (this.dedupTimer) {
|
||||
clearInterval(this.dedupTimer);
|
||||
|
|
@ -118,6 +194,7 @@ export class DingtalkChannel extends ChannelBase {
|
|||
const isGroup = data.conversationType === '2';
|
||||
const text = data.text?.content?.trim() || '';
|
||||
const sessionWebhook = data.sessionWebhook;
|
||||
const conversationId = data.conversationId;
|
||||
|
||||
if (!sessionWebhook) {
|
||||
process.stderr.write(
|
||||
|
|
@ -126,36 +203,61 @@ export class DingtalkChannel extends ChannelBase {
|
|||
return;
|
||||
}
|
||||
|
||||
// Cache webhook by conversationId so sendMessage can look it up
|
||||
if (conversationId) {
|
||||
this.webhooks.set(conversationId, sessionWebhook);
|
||||
}
|
||||
|
||||
// In group chats, check isInAtList from the raw data
|
||||
const rawData = JSON.parse(downstream.data);
|
||||
const rawData =
|
||||
typeof downstream.data === 'string'
|
||||
? JSON.parse(downstream.data)
|
||||
: downstream.data;
|
||||
const isMentioned = Boolean(rawData.isInAtList);
|
||||
|
||||
// Strip @bot mention from text
|
||||
let cleanText = text;
|
||||
if (isMentioned && data.senderNick) {
|
||||
// DingTalk prepends the @mention text; remove it
|
||||
if (isMentioned) {
|
||||
cleanText = text.replace(/@\S+/g, '').trim();
|
||||
}
|
||||
|
||||
const chatId = conversationId || sessionWebhook;
|
||||
|
||||
const envelope: Envelope = {
|
||||
channelName: this.name,
|
||||
senderId: data.senderId || data.senderStaffId,
|
||||
senderName: data.senderNick || 'Unknown',
|
||||
chatId: sessionWebhook, // Use webhook URL as chatId for sendMessage
|
||||
chatId,
|
||||
text: cleanText || text,
|
||||
isGroup,
|
||||
isMentioned,
|
||||
isReplyToBot: false,
|
||||
};
|
||||
|
||||
// Attach 👀 reaction, process message, then recall reaction
|
||||
const reactionMsgId = msgId;
|
||||
const reactionConvId = conversationId;
|
||||
|
||||
const processMessage = async () => {
|
||||
if (reactionMsgId && reactionConvId) {
|
||||
this.attachReaction(reactionMsgId, reactionConvId).catch(() => {});
|
||||
}
|
||||
try {
|
||||
await this.handleInbound(envelope);
|
||||
} finally {
|
||||
if (reactionMsgId && reactionConvId) {
|
||||
this.recallReaction(reactionMsgId, reactionConvId).catch(() => {});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Don't await — stream callback should return quickly
|
||||
this.handleInbound(envelope).catch((err) => {
|
||||
processMessage().catch((err) => {
|
||||
process.stderr.write(
|
||||
`[DingTalk:${this.name}] Error handling message: ${err}\n`,
|
||||
);
|
||||
// Try to send error reply
|
||||
this.sendMessage(
|
||||
sessionWebhook,
|
||||
chatId,
|
||||
'Sorry, something went wrong processing your message.',
|
||||
).catch(() => {});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue