Merge pull request #76 from divingleee/feat/dingtalk-at-person

dingtalk add @person function
This commit is contained in:
xiemanR 2026-08-28 21:44:54 +08:00 • committed by GitHub
commit 799391552e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 32 additions and 19 deletions

View file

@ -1008,13 +1008,18 @@ export function createDingtalkApi({
failAiCard: failCard, failAiCard: failCard,
async sendText({ clientId, clientSecret, sessionWebhook, text, signal }) { async sendText({ clientId, clientSecret, sessionWebhook, text, at, signal }) {
const content = nonEmptyString(text); const content = nonEmptyString(text);
if (!content) throw new TypeError('text is required'); if (!content) throw new TypeError('text is required');
const webhook = normalizeDingtalkSessionWebhook(sessionWebhook); const webhook = normalizeDingtalkSessionWebhook(sessionWebhook);
const token = await accessToken({ clientId, clientSecret, signal }); 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, { const response = await requestJson(fetchImpl, webhook, {
body: { msgtype: 'text', text: { content } }, body: { msgtype: 'text', text: { content }, ...replyAt },
headers: { 'x-acs-dingtalk-access-token': token }, headers: { 'x-acs-dingtalk-access-token': token },
signal, signal,
action: '消息回复', action: '消息回复',

View file

@ -551,7 +551,7 @@ export class DingtalkHarnessBridge {
`[dsh-dingtalk] failed to process a command [${failure.referenceId}]`, `[dsh-dingtalk] failed to process a command [${failure.referenceId}]`,
safeErrorDiagnostic(error), safeErrorDiagnostic(error),
); );
return this.#send(sessionWebhook, messageFailureText(failure)).catch(() => undefined); return this.#send(sessionWebhook, messageFailureText(failure), this.#atUsersFor(message)).catch(() => undefined);
}).finally(() => { }).finally(() => {
this.#acceptedMessageIds.delete(messageId); this.#acceptedMessageIds.delete(messageId);
this.#commandTasks.delete(task); this.#commandTasks.delete(task);
@ -573,7 +573,7 @@ export class DingtalkHarnessBridge {
: null, : null,
isQuestionPending: () => this.#pendingInteractions.has(key), isQuestionPending: () => this.#pendingInteractions.has(key),
send: sessionWebhook send: sessionWebhook
? (reply) => this.#send(sessionWebhook, reply) ? (reply) => this.#send(sessionWebhook, reply, this.#atUsersFor(message))
: async () => undefined, : async () => undefined,
}); });
if (approvalReply) { if (approvalReply) {
@ -807,7 +807,7 @@ export class DingtalkHarnessBridge {
]); ]);
} }
for (const reply of result?.messages ?? [result?.message]) { 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; this.#status.lastError = null;
} }
@ -885,23 +885,23 @@ export class DingtalkHarnessBridge {
let batchSettled = batchSubmission === null; let batchSettled = batchSubmission === null;
try { try {
if (!text && !hasImages && !hasFiles) { if (!text && !hasImages && !hasFiles) {
await this.#send(sessionWebhook, t('目前支持文字、图片和文件消息。')); await this.#send(sessionWebhook, t('目前支持文字、图片和文件消息。'), this.#atUsersFor(message));
return; return;
} }
const command = text.toLowerCase(); const command = text.toLowerCase();
if (isPlainText && !hasImages && !hasFiles && command === '/help') { if (isPlainText && !hasImages && !hasFiles && command === '/help') {
await this.#send(sessionWebhook, helpText()); await this.#send(sessionWebhook, helpText(), this.#atUsersFor(message));
return; return;
} }
if (isPlainText && !hasImages && !hasFiles && command === '/status') { if (isPlainText && !hasImages && !hasFiles && command === '/status') {
await this.#harness.ensureRunning({ signal: this.#signal }); 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; return;
} }
if (isPlainText && !hasImages && !hasFiles && command === '/new') { if (isPlainText && !hasImages && !hasFiles && command === '/new') {
await this.#state.clearSession(key); await this.#state.clearSession(key);
await this.#send(sessionWebhook, t('已开启新会话。请发送你的问题。')); await this.#send(sessionWebhook, t('已开启新会话。请发送你的问题。'), this.#atUsersFor(message));
return; return;
} }
const workspaceCommand = isPlainText && !hasImages && !hasFiles const workspaceCommand = isPlainText && !hasImages && !hasFiles
@ -909,7 +909,7 @@ export class DingtalkHarnessBridge {
: null; : null;
if (workspaceCommand) { if (workspaceCommand) {
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) { for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
await this.#send(sessionWebhook, reply); await this.#send(sessionWebhook, reply, this.#atUsersFor(message));
} }
return; return;
} }
@ -923,7 +923,7 @@ export class DingtalkHarnessBridge {
) )
: null; : null;
if (compactCommand) { if (compactCommand) {
await this.#send(sessionWebhook, compactCommand.message); await this.#send(sessionWebhook, compactCommand.message, this.#atUsersFor(message));
return; return;
} }
@ -988,7 +988,7 @@ export class DingtalkHarnessBridge {
textReceipt = createDeliveryReceipt({ textReceipt = createDeliveryReceipt({
deliveryId: messageId, deliveryId: messageId,
presentation: 'dingtalk-text', presentation: 'dingtalk-text',
providerMessageIds: await this.#send(sessionWebhook, answerText), providerMessageIds: await this.#send(sessionWebhook, answerText, this.#atUsersFor(message)),
}); });
} }
} catch (error) { } catch (error) {
@ -1049,7 +1049,7 @@ export class DingtalkHarnessBridge {
? `${errorText}\n\n${batchFailureMessage}` ? `${errorText}\n\n${batchFailureMessage}`
: errorText; : errorText;
const streamed = cardStarted && await cardStream.finish(visibleError); 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 { } catch {
this.#logger.error?.('[dsh-dingtalk] failed to send the safe error reply'); this.#logger.error?.('[dsh-dingtalk] failed to send the safe error reply');
} }
@ -1102,7 +1102,7 @@ export class DingtalkHarnessBridge {
const text = message?.msgtype === 'text' ? nonEmptyString(message?.text?.content) : null; const text = message?.msgtype === 'text' ? nonEmptyString(message?.text?.content) : null;
if (!text) { if (!text) {
try { try {
await this.#send(sessionWebhook, t('请用文字回答当前问题。')); await this.#send(sessionWebhook, t('请用文字回答当前问题。'), this.#atUsersFor(message));
} catch { } catch {
this.#logger.error?.('[dsh-dingtalk] failed to reject a non-text interaction reply'); this.#logger.error?.('[dsh-dingtalk] failed to reject a non-text interaction reply');
} }
@ -1113,7 +1113,7 @@ export class DingtalkHarnessBridge {
if (!pending || pending !== expected || pending.submitting) { if (!pending || pending !== expected || pending.submitting) {
if (claimed && (!pending || pending !== expected)) { if (claimed && (!pending || pending !== expected)) {
try { try {
await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT)); await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT), this.#atUsersFor(message));
} catch { } catch {
this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice'); this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice');
} }
@ -1178,7 +1178,7 @@ export class DingtalkHarnessBridge {
if (error?.code === 'interaction-not-pending') { if (error?.code === 'interaction-not-pending') {
this.#clearPendingInteraction(key, pending.interactionId); this.#clearPendingInteraction(key, pending.interactionId);
try { try {
await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT)); await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT), this.#atUsersFor(message));
} catch { } catch {
this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice'); this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice');
} }
@ -1191,7 +1191,7 @@ export class DingtalkHarnessBridge {
this.#status.lastError = t('回答提交失败。'); this.#status.lastError = t('回答提交失败。');
this.#logger.error?.('[dsh-dingtalk] failed to answer a Harness interaction'); this.#logger.error?.('[dsh-dingtalk] failed to answer a Harness interaction');
try { try {
await this.#send(sessionWebhook, t('回答提交失败,请重新发送当前问题的答案。')); await this.#send(sessionWebhook, t('回答提交失败,请重新发送当前问题的答案。'), this.#atUsersFor(message));
} catch { } catch {
this.#logger.error?.('[dsh-dingtalk] failed to send an interaction retry notice'); this.#logger.error?.('[dsh-dingtalk] failed to send an interaction retry notice');
} }
@ -1323,7 +1323,7 @@ export class DingtalkHarnessBridge {
return; return;
} }
try { try {
await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT)); await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT), this.#atUsersFor(message));
} catch { } catch {
this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice'); this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice');
} }
@ -1367,7 +1367,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 = []; const providerMessageIds = [];
for (const chunk of splitDingtalkText(text, this.#maxMessageChars)) { for (const chunk of splitDingtalkText(text, this.#maxMessageChars)) {
this.#signal?.throwIfAborted(); this.#signal?.throwIfAborted();
@ -1376,6 +1383,7 @@ export class DingtalkHarnessBridge {
clientSecret: this.#clientSecret, clientSecret: this.#clientSecret,
sessionWebhook, sessionWebhook,
text: chunk, text: chunk,
at,
signal: this.#signal, signal: this.#signal,
}); });
providerMessageIds.push(...providerMessageIdsFor(result)); providerMessageIds.push(...providerMessageIdsFor(result));