@yaoyuanchao/dingtalk 1.4.20 β 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/package.json +1 -1
- package/src/api.ts +264 -0
- package/src/monitor.ts +72 -5
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.5.0] - 2026-01-31
|
|
9
|
+
|
|
10
|
+
### π New Feature: Typing Indicator with Auto-Recall
|
|
11
|
+
|
|
12
|
+
- **Typing Indicator** β When processing a message, automatically sends "β³ ζθδΈ..." which is silently recalled when the reply is ready. Much better UX than the old `showThinking` option.
|
|
13
|
+
- **Message Recall APIs** β New functions in `api.ts`:
|
|
14
|
+
- `sendDMMessageWithKey()` β Send DM and return processQueryKey for recall
|
|
15
|
+
- `sendGroupMessageWithKey()` β Send group message and return processQueryKey
|
|
16
|
+
- `recallDMMessages()` β Batch recall DM messages
|
|
17
|
+
- `recallGroupMessages()` β Batch recall group messages
|
|
18
|
+
- `sendTypingIndicator()` β One-stop helper that returns a cleanup function
|
|
19
|
+
|
|
20
|
+
### Configuration
|
|
21
|
+
|
|
22
|
+
- `typingIndicator: false` β Disable typing indicator (default: enabled)
|
|
23
|
+
- `typingIndicatorMessage: "xxx"` β Customize the thinking message
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Deprecated `showThinking` option (still works as fallback if `typingIndicator` is explicitly disabled)
|
|
28
|
+
|
|
8
29
|
## [1.4.10] - 2026-01-30
|
|
9
30
|
|
|
10
31
|
### Fixed
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -689,3 +689,267 @@ export function textToMarkdownFile(text: string, title?: string): { buffer: Buff
|
|
|
689
689
|
|
|
690
690
|
return { buffer, fileName };
|
|
691
691
|
}
|
|
692
|
+
|
|
693
|
+
// ============================================================================
|
|
694
|
+
// Message Recall (ζ€ε) APIs
|
|
695
|
+
// ============================================================================
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Send a DM message and return the processQueryKey for later recall
|
|
699
|
+
* This is an enhanced version that returns the message ID
|
|
700
|
+
*/
|
|
701
|
+
export async function sendDMMessageWithKey(params: {
|
|
702
|
+
clientId: string;
|
|
703
|
+
clientSecret: string;
|
|
704
|
+
robotCode: string;
|
|
705
|
+
userId: string;
|
|
706
|
+
text: string;
|
|
707
|
+
format?: 'text' | 'markdown';
|
|
708
|
+
}): Promise<{ ok: boolean; processQueryKey?: string; error?: string }> {
|
|
709
|
+
try {
|
|
710
|
+
const token = await getDingTalkAccessToken(params.clientId, params.clientSecret);
|
|
711
|
+
const headers = { "x-acs-dingtalk-access-token": token };
|
|
712
|
+
|
|
713
|
+
const useMarkdown = params.format !== 'text';
|
|
714
|
+
const msgKey = useMarkdown ? 'sampleMarkdown' : 'sampleText';
|
|
715
|
+
const msgParam = useMarkdown
|
|
716
|
+
? JSON.stringify({ title: 'AI', text: params.text })
|
|
717
|
+
: JSON.stringify({ content: params.text });
|
|
718
|
+
|
|
719
|
+
const res = await jsonPost(
|
|
720
|
+
`${DINGTALK_API_BASE}/robot/oToMessages/batchSend`,
|
|
721
|
+
{
|
|
722
|
+
robotCode: params.robotCode,
|
|
723
|
+
userIds: [params.userId],
|
|
724
|
+
msgKey,
|
|
725
|
+
msgParam,
|
|
726
|
+
},
|
|
727
|
+
headers,
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
if (res?.code || (res?.errcode && res.errcode !== 0)) {
|
|
731
|
+
console.warn(`[dingtalk] DM send error:`, res);
|
|
732
|
+
return { ok: false, error: res.message || res.errmsg };
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return {
|
|
736
|
+
ok: true,
|
|
737
|
+
processQueryKey: res.processQueryKey
|
|
738
|
+
};
|
|
739
|
+
} catch (err) {
|
|
740
|
+
console.warn(`[dingtalk] Error sending DM:`, err);
|
|
741
|
+
return { ok: false, error: String(err) };
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Send a group message and return the processQueryKey for later recall
|
|
747
|
+
*/
|
|
748
|
+
export async function sendGroupMessageWithKey(params: {
|
|
749
|
+
clientId: string;
|
|
750
|
+
clientSecret: string;
|
|
751
|
+
robotCode: string;
|
|
752
|
+
conversationId: string;
|
|
753
|
+
text: string;
|
|
754
|
+
format?: 'text' | 'markdown';
|
|
755
|
+
}): Promise<{ ok: boolean; processQueryKey?: string; error?: string }> {
|
|
756
|
+
try {
|
|
757
|
+
const token = await getDingTalkAccessToken(params.clientId, params.clientSecret);
|
|
758
|
+
const headers = { "x-acs-dingtalk-access-token": token };
|
|
759
|
+
|
|
760
|
+
const useMarkdown = params.format !== 'text';
|
|
761
|
+
const msgKey = useMarkdown ? 'sampleMarkdown' : 'sampleText';
|
|
762
|
+
const msgParam = useMarkdown
|
|
763
|
+
? JSON.stringify({ title: 'AI', text: params.text })
|
|
764
|
+
: JSON.stringify({ content: params.text });
|
|
765
|
+
|
|
766
|
+
const res = await jsonPost(
|
|
767
|
+
`${DINGTALK_API_BASE}/robot/groupMessages/send`,
|
|
768
|
+
{
|
|
769
|
+
robotCode: params.robotCode,
|
|
770
|
+
openConversationId: params.conversationId,
|
|
771
|
+
msgKey,
|
|
772
|
+
msgParam,
|
|
773
|
+
},
|
|
774
|
+
headers,
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
if (res?.code || (res?.errcode && res.errcode !== 0)) {
|
|
778
|
+
console.warn(`[dingtalk] Group send error:`, res);
|
|
779
|
+
return { ok: false, error: res.message || res.errmsg };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return {
|
|
783
|
+
ok: true,
|
|
784
|
+
processQueryKey: res.processQueryKey
|
|
785
|
+
};
|
|
786
|
+
} catch (err) {
|
|
787
|
+
console.warn(`[dingtalk] Error sending group message:`, err);
|
|
788
|
+
return { ok: false, error: String(err) };
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Recall (ζ€ε) DM messages
|
|
794
|
+
* Note: This is a "silent recall" - the message just disappears without notification
|
|
795
|
+
*/
|
|
796
|
+
export async function recallDMMessages(params: {
|
|
797
|
+
clientId: string;
|
|
798
|
+
clientSecret: string;
|
|
799
|
+
robotCode: string;
|
|
800
|
+
userId: string;
|
|
801
|
+
processQueryKeys: string[];
|
|
802
|
+
}): Promise<{ ok: boolean; successKeys?: string[]; failedKeys?: Record<string, string>; error?: string }> {
|
|
803
|
+
try {
|
|
804
|
+
const token = await getDingTalkAccessToken(params.clientId, params.clientSecret);
|
|
805
|
+
const headers = { "x-acs-dingtalk-access-token": token };
|
|
806
|
+
|
|
807
|
+
const res = await jsonPost(
|
|
808
|
+
`${DINGTALK_API_BASE}/robot/otoMessages/batchRecall`,
|
|
809
|
+
{
|
|
810
|
+
robotCode: params.robotCode,
|
|
811
|
+
chatBotUserId: params.userId,
|
|
812
|
+
processQueryKeys: params.processQueryKeys,
|
|
813
|
+
},
|
|
814
|
+
headers,
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
if (res?.code || (res?.errcode && res.errcode !== 0)) {
|
|
818
|
+
console.warn(`[dingtalk] DM recall error:`, res);
|
|
819
|
+
return { ok: false, error: res.message || res.errmsg };
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
return {
|
|
823
|
+
ok: true,
|
|
824
|
+
successKeys: res.successResult,
|
|
825
|
+
failedKeys: res.failedResult,
|
|
826
|
+
};
|
|
827
|
+
} catch (err) {
|
|
828
|
+
console.warn(`[dingtalk] Error recalling DM:`, err);
|
|
829
|
+
return { ok: false, error: String(err) };
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Recall (ζ€ε) group messages
|
|
835
|
+
*/
|
|
836
|
+
export async function recallGroupMessages(params: {
|
|
837
|
+
clientId: string;
|
|
838
|
+
clientSecret: string;
|
|
839
|
+
robotCode: string;
|
|
840
|
+
conversationId: string;
|
|
841
|
+
processQueryKeys: string[];
|
|
842
|
+
}): Promise<{ ok: boolean; successKeys?: string[]; failedKeys?: Record<string, string>; error?: string }> {
|
|
843
|
+
try {
|
|
844
|
+
const token = await getDingTalkAccessToken(params.clientId, params.clientSecret);
|
|
845
|
+
const headers = { "x-acs-dingtalk-access-token": token };
|
|
846
|
+
|
|
847
|
+
const res = await jsonPost(
|
|
848
|
+
`${DINGTALK_API_BASE}/robot/groupMessages/recall`,
|
|
849
|
+
{
|
|
850
|
+
robotCode: params.robotCode,
|
|
851
|
+
openConversationId: params.conversationId,
|
|
852
|
+
processQueryKeys: params.processQueryKeys,
|
|
853
|
+
},
|
|
854
|
+
headers,
|
|
855
|
+
);
|
|
856
|
+
|
|
857
|
+
if (res?.code || (res?.errcode && res.errcode !== 0)) {
|
|
858
|
+
console.warn(`[dingtalk] Group recall error:`, res);
|
|
859
|
+
return { ok: false, error: res.message || res.errmsg };
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return {
|
|
863
|
+
ok: true,
|
|
864
|
+
successKeys: res.successResult,
|
|
865
|
+
failedKeys: res.failedResult,
|
|
866
|
+
};
|
|
867
|
+
} catch (err) {
|
|
868
|
+
console.warn(`[dingtalk] Error recalling group message:`, err);
|
|
869
|
+
return { ok: false, error: String(err) };
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Typing indicator helper - sends a "thinking" message that will be recalled
|
|
875
|
+
* Returns a cleanup function to recall the message
|
|
876
|
+
*/
|
|
877
|
+
export async function sendTypingIndicator(params: {
|
|
878
|
+
clientId: string;
|
|
879
|
+
clientSecret: string;
|
|
880
|
+
robotCode: string;
|
|
881
|
+
userId?: string;
|
|
882
|
+
conversationId?: string;
|
|
883
|
+
message?: string;
|
|
884
|
+
}): Promise<{ cleanup: () => Promise<void>; error?: string }> {
|
|
885
|
+
const typingMessage = params.message || "β³ ζθδΈ...";
|
|
886
|
+
|
|
887
|
+
try {
|
|
888
|
+
if (params.userId) {
|
|
889
|
+
const result = await sendDMMessageWithKey({
|
|
890
|
+
clientId: params.clientId,
|
|
891
|
+
clientSecret: params.clientSecret,
|
|
892
|
+
robotCode: params.robotCode,
|
|
893
|
+
userId: params.userId,
|
|
894
|
+
text: typingMessage,
|
|
895
|
+
format: 'text',
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
if (!result.ok || !result.processQueryKey) {
|
|
899
|
+
return {
|
|
900
|
+
cleanup: async () => {},
|
|
901
|
+
error: result.error || "Failed to send typing indicator"
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const processQueryKey = result.processQueryKey;
|
|
906
|
+
return {
|
|
907
|
+
cleanup: async () => {
|
|
908
|
+
await recallDMMessages({
|
|
909
|
+
clientId: params.clientId,
|
|
910
|
+
clientSecret: params.clientSecret,
|
|
911
|
+
robotCode: params.robotCode,
|
|
912
|
+
userId: params.userId!,
|
|
913
|
+
processQueryKeys: [processQueryKey],
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (params.conversationId) {
|
|
920
|
+
const result = await sendGroupMessageWithKey({
|
|
921
|
+
clientId: params.clientId,
|
|
922
|
+
clientSecret: params.clientSecret,
|
|
923
|
+
robotCode: params.robotCode,
|
|
924
|
+
conversationId: params.conversationId,
|
|
925
|
+
text: typingMessage,
|
|
926
|
+
format: 'text',
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
if (!result.ok || !result.processQueryKey) {
|
|
930
|
+
return {
|
|
931
|
+
cleanup: async () => {},
|
|
932
|
+
error: result.error || "Failed to send typing indicator"
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const processQueryKey = result.processQueryKey;
|
|
937
|
+
return {
|
|
938
|
+
cleanup: async () => {
|
|
939
|
+
await recallGroupMessages({
|
|
940
|
+
clientId: params.clientId,
|
|
941
|
+
clientSecret: params.clientSecret,
|
|
942
|
+
robotCode: params.robotCode,
|
|
943
|
+
conversationId: params.conversationId!,
|
|
944
|
+
processQueryKeys: [processQueryKey],
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return { cleanup: async () => {}, error: "Either userId or conversationId required" };
|
|
951
|
+
} catch (err) {
|
|
952
|
+
console.warn(`[dingtalk] Error sending typing indicator:`, err);
|
|
953
|
+
return { cleanup: async () => {}, error: String(err) };
|
|
954
|
+
}
|
|
955
|
+
}
|
package/src/monitor.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DingTalkRobotMessage, ResolvedDingTalkAccount, ExtractedMessage } from "./types.js";
|
|
2
|
-
import { sendViaSessionWebhook, sendMarkdownViaSessionWebhook, sendDingTalkRestMessage, batchGetUserInfo, downloadPicture, downloadMediaFile, cleanupOldMedia, uploadMediaFile, sendFileMessage, textToMarkdownFile } from "./api.js";
|
|
2
|
+
import { sendViaSessionWebhook, sendMarkdownViaSessionWebhook, sendDingTalkRestMessage, batchGetUserInfo, downloadPicture, downloadMediaFile, cleanupOldMedia, uploadMediaFile, sendFileMessage, textToMarkdownFile, sendTypingIndicator } from "./api.js";
|
|
3
3
|
import { getDingTalkRuntime } from "./runtime.js";
|
|
4
4
|
|
|
5
5
|
// ============================================================================
|
|
@@ -853,11 +853,40 @@ async function dispatchMessage(params: {
|
|
|
853
853
|
const runtime = getDingTalkRuntime();
|
|
854
854
|
const isGroup = !isDm;
|
|
855
855
|
|
|
856
|
-
//
|
|
857
|
-
|
|
856
|
+
// Typing indicator cleanup function (will be called after dispatch completes)
|
|
857
|
+
let typingCleanup: (() => Promise<void>) | null = null;
|
|
858
|
+
|
|
859
|
+
// Send typing indicator (recallable) if enabled
|
|
860
|
+
// This replaces the old showThinking feature with a better UX - the indicator disappears when reply arrives
|
|
861
|
+
if (account.config.typingIndicator !== false && account.clientId && account.clientSecret) {
|
|
862
|
+
try {
|
|
863
|
+
const typingMessage = account.config.typingIndicatorMessage || 'β³ ζθδΈ...';
|
|
864
|
+
const robotCode = account.robotCode || account.clientId;
|
|
865
|
+
|
|
866
|
+
const result = await sendTypingIndicator({
|
|
867
|
+
clientId: account.clientId,
|
|
868
|
+
clientSecret: account.clientSecret,
|
|
869
|
+
robotCode,
|
|
870
|
+
userId: isDm ? senderId : undefined,
|
|
871
|
+
conversationId: !isDm ? conversationId : undefined,
|
|
872
|
+
message: typingMessage,
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
if (result.error) {
|
|
876
|
+
log?.info?.('[dingtalk] Typing indicator failed: ' + result.error);
|
|
877
|
+
} else {
|
|
878
|
+
typingCleanup = result.cleanup;
|
|
879
|
+
log?.info?.('[dingtalk] Typing indicator sent (will be recalled on reply)');
|
|
880
|
+
}
|
|
881
|
+
} catch (err) {
|
|
882
|
+
log?.info?.('[dingtalk] Typing indicator error: ' + err);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
// Legacy: Send thinking feedback (opt-in, non-recallable) - only if typingIndicator is explicitly disabled
|
|
886
|
+
else if (account.config.showThinking && replyTarget.sessionWebhook) {
|
|
858
887
|
try {
|
|
859
888
|
await sendViaSessionWebhook(replyTarget.sessionWebhook, 'ζ£ε¨ζθ...');
|
|
860
|
-
log?.info?.('[dingtalk] Sent thinking indicator');
|
|
889
|
+
log?.info?.('[dingtalk] Sent thinking indicator (legacy, non-recallable)');
|
|
861
890
|
} catch (_) {
|
|
862
891
|
// fire-and-forget, don't block processing
|
|
863
892
|
}
|
|
@@ -881,6 +910,20 @@ async function dispatchMessage(params: {
|
|
|
881
910
|
runtime?.channel?.reply?.dispatchReplyFromConfig
|
|
882
911
|
);
|
|
883
912
|
|
|
913
|
+
// Track if we've already cleaned up the typing indicator
|
|
914
|
+
let typingCleaned = false;
|
|
915
|
+
const cleanupTyping = async () => {
|
|
916
|
+
if (typingCleanup && !typingCleaned) {
|
|
917
|
+
typingCleaned = true;
|
|
918
|
+
try {
|
|
919
|
+
await typingCleanup();
|
|
920
|
+
log?.info?.('[dingtalk] Typing indicator recalled');
|
|
921
|
+
} catch (err) {
|
|
922
|
+
log?.info?.('[dingtalk] Failed to recall typing indicator: ' + err);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
|
|
884
927
|
try {
|
|
885
928
|
if (hasFullPipeline) {
|
|
886
929
|
// Full SDK pipeline: route β session β envelope β dispatch
|
|
@@ -888,6 +931,7 @@ async function dispatchMessage(params: {
|
|
|
888
931
|
runtime, msg, rawBody, account, cfg: actualCfg, sessionKey, isDm,
|
|
889
932
|
senderId, senderName, conversationId, replyTarget,
|
|
890
933
|
mediaPath, mediaType, log, setStatus,
|
|
934
|
+
onFirstReply: cleanupTyping,
|
|
891
935
|
});
|
|
892
936
|
} else if (runtime?.channel?.reply?.dispatchReplyWithBufferedBlockDispatcher) {
|
|
893
937
|
// Fallback: existing buffered block dispatcher
|
|
@@ -920,6 +964,9 @@ async function dispatchMessage(params: {
|
|
|
920
964
|
cfg: actualCfg,
|
|
921
965
|
dispatcherOptions: {
|
|
922
966
|
deliver: async (payload: any) => {
|
|
967
|
+
// Recall typing indicator on first delivery
|
|
968
|
+
await cleanupTyping();
|
|
969
|
+
|
|
923
970
|
log?.info?.("[dingtalk] Deliver payload keys: " + Object.keys(payload || {}).join(',') + " text?=" + (typeof payload?.text) + " markdown?=" + (typeof payload?.markdown));
|
|
924
971
|
const textToSend = resolveDeliverText(payload, log);
|
|
925
972
|
if (textToSend) {
|
|
@@ -930,10 +977,13 @@ async function dispatchMessage(params: {
|
|
|
930
977
|
}
|
|
931
978
|
},
|
|
932
979
|
onError: (err: any) => {
|
|
980
|
+
// Also cleanup on error
|
|
981
|
+
cleanupTyping().catch(() => {});
|
|
933
982
|
log?.info?.("[dingtalk] Reply error: " + err);
|
|
934
983
|
},
|
|
935
984
|
},
|
|
936
985
|
}).catch((err) => {
|
|
986
|
+
cleanupTyping().catch(() => {});
|
|
937
987
|
log?.info?.("[dingtalk] Dispatch failed: " + err);
|
|
938
988
|
});
|
|
939
989
|
|
|
@@ -941,8 +991,10 @@ async function dispatchMessage(params: {
|
|
|
941
991
|
runtime.channel?.activity?.record?.('dingtalk', account.accountId, 'message');
|
|
942
992
|
} else {
|
|
943
993
|
log?.info?.("[dingtalk] Runtime dispatch not available");
|
|
994
|
+
await cleanupTyping();
|
|
944
995
|
}
|
|
945
996
|
} catch (err) {
|
|
997
|
+
await cleanupTyping();
|
|
946
998
|
log?.info?.("[dingtalk] Dispatch error: " + err);
|
|
947
999
|
}
|
|
948
1000
|
}
|
|
@@ -967,10 +1019,13 @@ async function dispatchWithFullPipeline(params: {
|
|
|
967
1019
|
mediaType?: string;
|
|
968
1020
|
log?: any;
|
|
969
1021
|
setStatus?: (update: Record<string, unknown>) => void;
|
|
1022
|
+
onFirstReply?: () => Promise<void>;
|
|
970
1023
|
}): Promise<void> {
|
|
971
1024
|
const { runtime: rt, msg, rawBody, account, cfg, isDm,
|
|
972
1025
|
senderId, senderName, conversationId, replyTarget,
|
|
973
|
-
log, setStatus } = params;
|
|
1026
|
+
log, setStatus, onFirstReply } = params;
|
|
1027
|
+
|
|
1028
|
+
let firstReplyFired = false;
|
|
974
1029
|
|
|
975
1030
|
// 1. Resolve agent route
|
|
976
1031
|
const route = rt.channel.routing.resolveAgentRoute({
|
|
@@ -1025,6 +1080,14 @@ async function dispatchWithFullPipeline(params: {
|
|
|
1025
1080
|
const { dispatcher, replyOptions, markDispatchIdle } = rt.channel.reply.createReplyDispatcherWithTyping({
|
|
1026
1081
|
responsePrefix: '',
|
|
1027
1082
|
deliver: async (payload: any) => {
|
|
1083
|
+
// Recall typing indicator on first delivery
|
|
1084
|
+
if (!firstReplyFired && onFirstReply) {
|
|
1085
|
+
firstReplyFired = true;
|
|
1086
|
+
await onFirstReply().catch((err) => {
|
|
1087
|
+
log?.info?.("[dingtalk] onFirstReply error: " + err);
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1028
1091
|
try {
|
|
1029
1092
|
log?.info?.("[dingtalk] Pipeline deliver payload keys: " + Object.keys(payload || {}).join(',') + " text?=" + (typeof payload?.text) + " markdown?=" + (typeof payload?.markdown));
|
|
1030
1093
|
const textToSend = resolveDeliverText(payload, log);
|
|
@@ -1047,6 +1110,10 @@ async function dispatchWithFullPipeline(params: {
|
|
|
1047
1110
|
await rt.channel.reply.dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyOptions });
|
|
1048
1111
|
} finally {
|
|
1049
1112
|
markDispatchIdle();
|
|
1113
|
+
// Ensure typing indicator is cleaned up even if no reply was sent
|
|
1114
|
+
if (!firstReplyFired && onFirstReply) {
|
|
1115
|
+
await onFirstReply().catch(() => {});
|
|
1116
|
+
}
|
|
1050
1117
|
}
|
|
1051
1118
|
|
|
1052
1119
|
// 10. Record activity
|