From 7d5bc94f64e223c610b01d4da3bd16682d99011a Mon Sep 17 00:00:00 2001 From: lbq Date: Thu, 27 Aug 2026 22:09:06 +0800 Subject: [PATCH] dingtalk add @person function --- src/channels/dingtalk/dingtalk-api.mjs | 9 +++-- src/channels/dingtalk/dingtalk-bridge.mjs | 44 +++++++++++++---------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/channels/dingtalk/dingtalk-api.mjs b/src/channels/dingtalk/dingtalk-api.mjs index 67920bb..c11c82a 100644 --- a/src/channels/dingtalk/dingtalk-api.mjs +++ b/src/channels/dingtalk/dingtalk-api.mjs @@ -1008,13 +1008,18 @@ export function createDingtalkApi({ failAiCard: failCard, - async sendText({ clientId, clientSecret, sessionWebhook, text, signal }) { + async sendText({ clientId, clientSecret, sessionWebhook, text, at, signal }) { const content = nonEmptyString(text); if (!content) throw new TypeError('text is required'); const webhook = normalizeDingtalkSessionWebhook(sessionWebhook); const token = await accessToken({ clientId, clientSecret, signal }); + const replyAt = at && typeof at === 'object' + && ((Array.isArray(at.atUserIds) && at.atUserIds.length > 0) + || (Array.isArray(at.atMobiles) && at.atMobiles.length > 0) + || at.isAtAll === true) + ? { at } : {}; const response = await requestJson(fetchImpl, webhook, { - body: { msgtype: 'text', text: { content } }, + body: { msgtype: 'text', text: { content }, ...replyAt }, headers: { 'x-acs-dingtalk-access-token': token }, signal, action: '消息回复', diff --git a/src/channels/dingtalk/dingtalk-bridge.mjs b/src/channels/dingtalk/dingtalk-bridge.mjs index 6236543..2f7d759 100644 --- a/src/channels/dingtalk/dingtalk-bridge.mjs +++ b/src/channels/dingtalk/dingtalk-bridge.mjs @@ -548,7 +548,7 @@ export class DingtalkHarnessBridge { `[dsh-dingtalk] failed to process a command [${failure.referenceId}]`, safeErrorDiagnostic(error), ); - return this.#send(sessionWebhook, messageFailureText(failure)).catch(() => undefined); + return this.#send(sessionWebhook, messageFailureText(failure), this.#atUsersFor(message)).catch(() => undefined); }).finally(() => { this.#acceptedMessageIds.delete(messageId); this.#commandTasks.delete(task); @@ -570,7 +570,7 @@ export class DingtalkHarnessBridge { : null, isQuestionPending: () => this.#pendingInteractions.has(key), send: sessionWebhook - ? (reply) => this.#send(sessionWebhook, reply) + ? (reply) => this.#send(sessionWebhook, reply, this.#atUsersFor(message)) : async () => undefined, }); if (approvalReply) { @@ -803,7 +803,7 @@ export class DingtalkHarnessBridge { ]); } for (const reply of result?.messages ?? [result?.message]) { - if (reply) await this.#send(sessionWebhook, reply); + if (reply) await this.#send(sessionWebhook, reply, this.#atUsersFor(message)); } this.#status.lastError = null; } @@ -829,7 +829,7 @@ export class DingtalkHarnessBridge { `[dsh-dingtalk] failed to process a batch input message [${failure.referenceId}]`, safeErrorDiagnostic(error), ); - await this.#send(sessionWebhook, messageFailureText(failure)).catch(() => undefined); + await this.#send(sessionWebhook, messageFailureText(failure), this.#atUsersFor(message)).catch(() => undefined); }).finally(() => { this.#acceptedMessageIds.delete(messageId); this.#commandTasks.delete(task); @@ -881,23 +881,23 @@ export class DingtalkHarnessBridge { let batchSettled = batchSubmission === null; try { if (!text && !hasImages && !hasFiles) { - await this.#send(sessionWebhook, t('目前支持文字、图片和文件消息。')); + await this.#send(sessionWebhook, t('目前支持文字、图片和文件消息。'), this.#atUsersFor(message)); return; } const command = text.toLowerCase(); if (isPlainText && !hasImages && !hasFiles && command === '/help') { - await this.#send(sessionWebhook, helpText()); + await this.#send(sessionWebhook, helpText(), this.#atUsersFor(message)); return; } if (isPlainText && !hasImages && !hasFiles && command === '/status') { await this.#harness.ensureRunning({ signal: this.#signal }); - await this.#send(sessionWebhook, t('钉钉机器人与 DeepSeek Harness 连接正常。')); + await this.#send(sessionWebhook, t('钉钉机器人与 DeepSeek Harness 连接正常。'), this.#atUsersFor(message)); return; } if (isPlainText && !hasImages && !hasFiles && command === '/new') { await this.#state.clearSession(key); - await this.#send(sessionWebhook, t('已开启新会话。请发送你的问题。')); + await this.#send(sessionWebhook, t('已开启新会话。请发送你的问题。'), this.#atUsersFor(message)); return; } const workspaceCommand = isPlainText && !hasImages && !hasFiles @@ -905,7 +905,7 @@ export class DingtalkHarnessBridge { : null; if (workspaceCommand) { for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) { - await this.#send(sessionWebhook, reply); + await this.#send(sessionWebhook, reply, this.#atUsersFor(message)); } return; } @@ -919,7 +919,7 @@ export class DingtalkHarnessBridge { ) : null; if (compactCommand) { - await this.#send(sessionWebhook, compactCommand.message); + await this.#send(sessionWebhook, compactCommand.message, this.#atUsersFor(message)); return; } @@ -984,7 +984,7 @@ export class DingtalkHarnessBridge { textReceipt = createDeliveryReceipt({ deliveryId: messageId, presentation: 'dingtalk-text', - providerMessageIds: await this.#send(sessionWebhook, answerText), + providerMessageIds: await this.#send(sessionWebhook, answerText, this.#atUsersFor(message)), }); } } catch (error) { @@ -1045,7 +1045,7 @@ export class DingtalkHarnessBridge { ? `${errorText}\n\n${batchFailureMessage}` : errorText; const streamed = cardStarted && await cardStream.finish(visibleError); - if (!streamed) await this.#send(sessionWebhook, visibleError); + if (!streamed) await this.#send(sessionWebhook, visibleError, this.#atUsersFor(message)); } catch { this.#logger.error?.('[dsh-dingtalk] failed to send the safe error reply'); } @@ -1098,7 +1098,7 @@ export class DingtalkHarnessBridge { const text = message?.msgtype === 'text' ? nonEmptyString(message?.text?.content) : null; if (!text) { try { - await this.#send(sessionWebhook, t('请用文字回答当前问题。')); + await this.#send(sessionWebhook, t('请用文字回答当前问题。'), this.#atUsersFor(message)); } catch { this.#logger.error?.('[dsh-dingtalk] failed to reject a non-text interaction reply'); } @@ -1109,7 +1109,7 @@ export class DingtalkHarnessBridge { if (!pending || pending !== expected || pending.submitting) { if (claimed && (!pending || pending !== expected)) { try { - await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT)); + await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT), this.#atUsersFor(message)); } catch { this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice'); } @@ -1174,7 +1174,7 @@ export class DingtalkHarnessBridge { if (error?.code === 'interaction-not-pending') { this.#clearPendingInteraction(key, pending.interactionId); try { - await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT)); + await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT), this.#atUsersFor(message)); } catch { this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice'); } @@ -1187,7 +1187,7 @@ export class DingtalkHarnessBridge { this.#status.lastError = t('回答提交失败。'); this.#logger.error?.('[dsh-dingtalk] failed to answer a Harness interaction'); try { - await this.#send(sessionWebhook, t('回答提交失败,请重新发送当前问题的答案。')); + await this.#send(sessionWebhook, t('回答提交失败,请重新发送当前问题的答案。'), this.#atUsersFor(message)); } catch { this.#logger.error?.('[dsh-dingtalk] failed to send an interaction retry notice'); } @@ -1319,7 +1319,7 @@ export class DingtalkHarnessBridge { return; } try { - await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT)); + await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT), this.#atUsersFor(message)); } catch { this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice'); } @@ -1363,7 +1363,14 @@ export class DingtalkHarnessBridge { } } - async #send(sessionWebhook, text) { + #atUsersFor(message) { + const sender = senderStaffId(message); + return String(message?.conversationType) === '2' && sender + ? { atUserIds: [sender] } + : undefined; + } + + async #send(sessionWebhook, text, at) { const providerMessageIds = []; for (const chunk of splitDingtalkText(text, this.#maxMessageChars)) { this.#signal?.throwIfAborted(); @@ -1372,6 +1379,7 @@ export class DingtalkHarnessBridge { clientSecret: this.#clientSecret, sessionWebhook, text: chunk, + at, signal: this.#signal, }); providerMessageIds.push(...providerMessageIdsFor(result));