@soimy/dingtalk 3.5.1 → 3.5.3
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/README.md +13 -24
- package/openclaw.plugin.json +695 -0
- package/package.json +12 -7
- package/src/ack-reaction-service.ts +1 -1
- package/src/auth.ts +1 -1
- package/src/card/card-action-handler.ts +1 -1
- package/src/card/card-stop-handler.ts +1 -1
- package/src/card/card-streaming-mode.ts +30 -0
- package/src/card/reasoning-answer-split.ts +162 -0
- package/src/card/reasoning-block-assembler.ts +157 -0
- package/src/card-callback-service.ts +1 -1
- package/src/card-draft-controller.ts +117 -6
- package/src/card-service.ts +112 -1
- package/src/channel.ts +131 -96
- package/src/command/card-stop-command.ts +4 -22
- package/src/command/inbound-command-dispatch-service.ts +464 -0
- package/src/config-schema.ts +62 -38
- package/src/config.ts +25 -3
- package/src/docs-service.ts +5 -5
- package/src/http-client.ts +20 -0
- package/src/inbound-handler.ts +475 -501
- package/src/logger-context.ts +16 -2
- package/src/media-utils.ts +166 -10
- package/src/message-utils.ts +33 -5
- package/src/{attachment-text-extractor.ts → messaging/attachment-text-extractor.ts} +1 -1
- package/src/{quoted-file-service.ts → messaging/quoted-file-service.ts} +14 -9
- package/src/onboarding.ts +29 -0
- package/src/plugin-sdk-channel-actions-augment.ts +11 -0
- package/src/reply-strategy-card.ts +294 -28
- package/src/reply-strategy-markdown.ts +124 -19
- package/src/reply-strategy.ts +22 -2
- package/src/send-service.ts +178 -7
- package/src/targeting/agent-routing.ts +55 -32
- package/src/{group-members-store.ts → targeting/group-members-store.ts} +1 -1
- package/src/types.ts +60 -4
- package/src/utils.ts +190 -0
package/src/channel.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { DWClient, TOPIC_CARD, TOPIC_ROBOT } from "dingtalk-stream";
|
|
3
|
+
import { jsonResult } from "openclaw/plugin-sdk/channel-actions";
|
|
3
4
|
import type { ChannelMessageActionAdapter } from "openclaw/plugin-sdk/channel-contract";
|
|
4
5
|
import { buildChannelConfigSchema, type OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
5
|
-
import { jsonResult } from "openclaw/plugin-sdk/telegram-core";
|
|
6
6
|
import { readStringParam } from "openclaw/plugin-sdk/param-readers";
|
|
7
7
|
import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
|
|
8
8
|
import { getAccessToken } from "./auth";
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
recordExplicitFeedbackLearning,
|
|
34
34
|
} from "./feedback-learning-service";
|
|
35
35
|
import { handleDingTalkMessage } from "./inbound-handler";
|
|
36
|
-
import { getLogger } from "./logger-context";
|
|
36
|
+
import { getLogger, setCurrentLogger } from "./logger-context";
|
|
37
37
|
import { prepareMediaInput, resolveOutboundMediaType } from "./media-utils";
|
|
38
38
|
import { dingtalkSetupAdapter, dingtalkSetupWizard } from "./onboarding.js";
|
|
39
39
|
import { resolveOriginalPeerId, preloadPeerIdsFromSessions } from "./peer-id-registry";
|
|
@@ -62,11 +62,14 @@ import type {
|
|
|
62
62
|
} from "./types";
|
|
63
63
|
import { ConnectionState } from "./types";
|
|
64
64
|
import {
|
|
65
|
+
closePluginDebugLog,
|
|
65
66
|
cleanupOrphanedTempFiles,
|
|
66
67
|
createResolve4FallbackLookup,
|
|
67
68
|
formatDingTalkConnectionErrorLog,
|
|
68
69
|
formatDingTalkErrorPayloadLog,
|
|
69
70
|
getCurrentTimestamp,
|
|
71
|
+
parseBooleanLike,
|
|
72
|
+
resolvePluginDebugLog,
|
|
70
73
|
} from "./utils";
|
|
71
74
|
|
|
72
75
|
type InstrumentedDWClient = {
|
|
@@ -186,29 +189,15 @@ function logInboundCounters(log: any, accountId: string, reason: string): void {
|
|
|
186
189
|
}
|
|
187
190
|
|
|
188
191
|
function readBooleanLikeParam(params: Record<string, unknown>, key: string): boolean | undefined {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
if (value === 0) {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
return undefined;
|
|
201
|
-
}
|
|
202
|
-
if (typeof value === "string") {
|
|
203
|
-
const normalized = value.trim().toLowerCase();
|
|
204
|
-
if (["1", "true", "yes", "y", "on"].includes(normalized)) {
|
|
205
|
-
return true;
|
|
206
|
-
}
|
|
207
|
-
if (["0", "false", "no", "n", "off"].includes(normalized)) {
|
|
208
|
-
return false;
|
|
209
|
-
}
|
|
192
|
+
return parseBooleanLike(params[key]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function readSharedAudioAsVoiceParam(params: Record<string, unknown>): boolean {
|
|
196
|
+
const sharedValue = readBooleanLikeParam(params, "audioAsVoice");
|
|
197
|
+
if (sharedValue !== undefined) {
|
|
198
|
+
return sharedValue;
|
|
210
199
|
}
|
|
211
|
-
return
|
|
200
|
+
return readBooleanLikeParam(params, "asVoice") === true;
|
|
212
201
|
}
|
|
213
202
|
|
|
214
203
|
function describeDingTalkMessageTool(cfg: OpenClawConfig): {
|
|
@@ -259,7 +248,7 @@ const dingtalkMessageActions: ChannelMessageActionAdapter = {
|
|
|
259
248
|
message = caption;
|
|
260
249
|
}
|
|
261
250
|
|
|
262
|
-
const asVoice =
|
|
251
|
+
const asVoice = readSharedAudioAsVoiceParam(params);
|
|
263
252
|
const requestedMediaType = readStringParam(params, "mediaType");
|
|
264
253
|
|
|
265
254
|
const target = resolveOriginalPeerId(stripTargetPrefix(to).targetId);
|
|
@@ -460,14 +449,15 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
460
449
|
const storePath = rt.channel.session.resolveStorePath(cfg.session?.store, {
|
|
461
450
|
agentId: accountId,
|
|
462
451
|
});
|
|
452
|
+
const effectiveLog = getLogger(accountId) || log;
|
|
463
453
|
try {
|
|
464
454
|
const result = await sendMessage(config, to, text, {
|
|
465
|
-
log,
|
|
455
|
+
log: effectiveLog,
|
|
466
456
|
accountId,
|
|
467
457
|
storePath,
|
|
468
458
|
conversationId: to,
|
|
469
459
|
});
|
|
470
|
-
|
|
460
|
+
effectiveLog?.debug?.(`[DingTalk] sendText: "${text}" result: ${JSON.stringify(result)}`);
|
|
471
461
|
if (!result.ok) {
|
|
472
462
|
throw new Error(result.error || "sendText failed");
|
|
473
463
|
}
|
|
@@ -487,7 +477,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
487
477
|
};
|
|
488
478
|
} catch (err: any) {
|
|
489
479
|
if (err?.response?.data !== undefined) {
|
|
490
|
-
|
|
480
|
+
effectiveLog?.error?.(formatDingTalkErrorPayloadLog("outbound.sendText", err.response.data));
|
|
491
481
|
}
|
|
492
482
|
throw new Error(
|
|
493
483
|
typeof err?.response?.data === "string"
|
|
@@ -504,6 +494,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
504
494
|
filePath,
|
|
505
495
|
mediaUrl,
|
|
506
496
|
mediaType: providedMediaType,
|
|
497
|
+
audioAsVoice,
|
|
507
498
|
asVoice,
|
|
508
499
|
accountId,
|
|
509
500
|
mediaLocalRoots,
|
|
@@ -514,6 +505,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
514
505
|
const storePath = rt.channel.session.resolveStorePath(cfg.session?.store, {
|
|
515
506
|
agentId: accountId,
|
|
516
507
|
});
|
|
508
|
+
const effectiveLog = getLogger(accountId) || log;
|
|
517
509
|
if (!config.clientId) {
|
|
518
510
|
throw new Error("DingTalk not configured");
|
|
519
511
|
}
|
|
@@ -521,7 +513,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
521
513
|
// Support mediaPath/filePath/mediaUrl aliases for better CLI compatibility.
|
|
522
514
|
const rawMediaPath = mediaPath || filePath || mediaUrl;
|
|
523
515
|
|
|
524
|
-
|
|
516
|
+
effectiveLog?.debug?.(
|
|
525
517
|
`[DingTalk] sendMedia called: to=${to}, mediaPath=${mediaPath}, filePath=${filePath}, mediaUrl=${mediaUrl}, rawMediaPath=${rawMediaPath}`,
|
|
526
518
|
);
|
|
527
519
|
|
|
@@ -539,10 +531,10 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
539
531
|
let preparedMedia;
|
|
540
532
|
try {
|
|
541
533
|
try {
|
|
542
|
-
preparedMedia = await prepareMediaInput(rawMediaPath,
|
|
534
|
+
preparedMedia = await prepareMediaInput(rawMediaPath, effectiveLog, config.mediaUrlAllowlist);
|
|
543
535
|
} catch (err: any) {
|
|
544
536
|
if (err?.response?.data !== undefined) {
|
|
545
|
-
|
|
537
|
+
effectiveLog?.error?.(
|
|
546
538
|
formatDingTalkErrorPayloadLog("outbound.sendMedia.prepare", err.response.data),
|
|
547
539
|
);
|
|
548
540
|
}
|
|
@@ -559,19 +551,19 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
559
551
|
? preparedMedia.path
|
|
560
552
|
: resolveRelativePath(preparedMedia.path);
|
|
561
553
|
|
|
562
|
-
|
|
554
|
+
effectiveLog?.debug?.(
|
|
563
555
|
`[DingTalk] sendMedia resolved path: rawMediaPath=${rawMediaPath}, actualMediaPath=${actualMediaPath}`,
|
|
564
556
|
);
|
|
565
557
|
|
|
566
558
|
const mediaType = resolveOutboundMediaType({
|
|
567
559
|
mediaType: typeof providedMediaType === "string" ? providedMediaType : undefined,
|
|
568
560
|
mediaPath: actualMediaPath,
|
|
569
|
-
asVoice: asVoice
|
|
561
|
+
asVoice: readSharedAudioAsVoiceParam({ audioAsVoice, asVoice }),
|
|
570
562
|
});
|
|
571
563
|
let result;
|
|
572
564
|
try {
|
|
573
565
|
result = await sendProactiveMedia(config, to, actualMediaPath, mediaType, {
|
|
574
|
-
log,
|
|
566
|
+
log: effectiveLog,
|
|
575
567
|
accountId,
|
|
576
568
|
storePath,
|
|
577
569
|
conversationId: to,
|
|
@@ -579,7 +571,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
579
571
|
});
|
|
580
572
|
} catch (err: any) {
|
|
581
573
|
if (err?.response?.data !== undefined) {
|
|
582
|
-
|
|
574
|
+
effectiveLog?.error?.(
|
|
583
575
|
formatDingTalkErrorPayloadLog("outbound.sendMedia.send", err.response.data),
|
|
584
576
|
);
|
|
585
577
|
}
|
|
@@ -587,7 +579,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
587
579
|
cause: err,
|
|
588
580
|
});
|
|
589
581
|
}
|
|
590
|
-
|
|
582
|
+
effectiveLog?.debug?.(
|
|
591
583
|
`[DingTalk] sendMedia: ${mediaType} file=${actualMediaPath} result: ${JSON.stringify(result)}`,
|
|
592
584
|
);
|
|
593
585
|
|
|
@@ -609,7 +601,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
609
601
|
);
|
|
610
602
|
} catch (err: any) {
|
|
611
603
|
if (err?.response?.data !== undefined) {
|
|
612
|
-
|
|
604
|
+
effectiveLog?.error?.(formatDingTalkErrorPayloadLog("outbound.sendMedia", err.response.data));
|
|
613
605
|
}
|
|
614
606
|
throw new Error(
|
|
615
607
|
typeof err?.response?.data === "string"
|
|
@@ -639,34 +631,48 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
639
631
|
accountStorePath = undefined;
|
|
640
632
|
}
|
|
641
633
|
|
|
642
|
-
|
|
634
|
+
const pluginLog = resolvePluginDebugLog({
|
|
635
|
+
accountId: account.accountId,
|
|
636
|
+
storePath: accountStorePath,
|
|
637
|
+
debug: config.debug,
|
|
638
|
+
baseLog: ctx.log,
|
|
639
|
+
});
|
|
640
|
+
setCurrentLogger(pluginLog, account.accountId);
|
|
641
|
+
|
|
642
|
+
pluginLog?.info?.(`[${account.accountId}] Initializing DingTalk Stream client...`);
|
|
643
643
|
|
|
644
644
|
// Preload known peer IDs from sessions so outbound delivery (e.g. cron
|
|
645
645
|
// jobs that fire immediately after startup) can resolve the original
|
|
646
646
|
// case-sensitive conversationId before any inbound message has arrived.
|
|
647
647
|
preloadPeerIdsFromSessions();
|
|
648
|
-
|
|
648
|
+
pluginLog?.debug?.(`[${account.accountId}] Peer ID registry preloaded from sessions`);
|
|
649
649
|
|
|
650
|
-
cleanupOrphanedTempFiles(
|
|
650
|
+
cleanupOrphanedTempFiles(pluginLog);
|
|
651
651
|
try {
|
|
652
652
|
const recovered = await recoverPendingCardsForAccount(
|
|
653
653
|
config,
|
|
654
654
|
account.accountId,
|
|
655
655
|
accountStorePath,
|
|
656
|
-
|
|
656
|
+
pluginLog,
|
|
657
657
|
);
|
|
658
658
|
if (recovered > 0) {
|
|
659
|
-
|
|
659
|
+
pluginLog?.info?.(
|
|
660
660
|
`[${account.accountId}] Recovered and finalized ${recovered} unfinished card(s) from previous runtime`,
|
|
661
661
|
);
|
|
662
662
|
}
|
|
663
663
|
} catch (err: any) {
|
|
664
|
-
|
|
664
|
+
pluginLog?.warn?.(
|
|
665
665
|
`[${account.accountId}] Failed to recover unfinished cards: ${err.message}`,
|
|
666
666
|
);
|
|
667
667
|
}
|
|
668
668
|
|
|
669
669
|
const useConnectionManager = config.useConnectionManager ?? true;
|
|
670
|
+
const applyStatusPatch = (patch: Record<string, unknown>) => {
|
|
671
|
+
ctx.setStatus({
|
|
672
|
+
...ctx.getStatus(),
|
|
673
|
+
...patch,
|
|
674
|
+
});
|
|
675
|
+
};
|
|
670
676
|
|
|
671
677
|
// Factory that creates a fresh DWClient with the TOPIC_ROBOT callback
|
|
672
678
|
// already registered. Each client captures its own reference for
|
|
@@ -683,7 +689,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
683
689
|
});
|
|
684
690
|
(c as any).sslopts = {
|
|
685
691
|
...(c as any).sslopts,
|
|
686
|
-
lookup: createResolve4FallbackLookup(
|
|
692
|
+
lookup: createResolve4FallbackLookup(pluginLog, account.accountId),
|
|
687
693
|
};
|
|
688
694
|
|
|
689
695
|
instrumentConnectionStages(c);
|
|
@@ -702,13 +708,21 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
702
708
|
c.socketCallBackResponse(messageId, { success: true });
|
|
703
709
|
stats.acked += 1;
|
|
704
710
|
} catch (ackError: any) {
|
|
705
|
-
|
|
711
|
+
pluginLog?.warn?.(
|
|
706
712
|
`[${account.accountId}] Failed to acknowledge callback ${messageId}: ${ackError.message}`,
|
|
707
713
|
);
|
|
708
714
|
}
|
|
709
715
|
};
|
|
710
716
|
try {
|
|
711
717
|
const data = JSON.parse(res.data) as DingTalkInboundMessage;
|
|
718
|
+
// Record the latest inbound callback arrival for status/UI projection.
|
|
719
|
+
// This intentionally tracks "message reached the plugin callback" rather
|
|
720
|
+
// than "message passed dedup and completed processing".
|
|
721
|
+
applyStatusPatch({
|
|
722
|
+
connected: true,
|
|
723
|
+
lastInboundAt: getCurrentTimestamp(),
|
|
724
|
+
lastEventAt: getCurrentTimestamp(),
|
|
725
|
+
});
|
|
712
726
|
|
|
713
727
|
const robotKey = resolveRobotCode(config) || account.accountId;
|
|
714
728
|
const msgId = data.msgId || messageId;
|
|
@@ -723,38 +737,38 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
723
737
|
accountId: account.accountId,
|
|
724
738
|
data,
|
|
725
739
|
sessionWebhook: data.sessionWebhook,
|
|
726
|
-
log:
|
|
740
|
+
log: pluginLog,
|
|
727
741
|
dingtalkConfig: config,
|
|
728
742
|
});
|
|
729
743
|
stats.processed += 1;
|
|
730
744
|
if (stats.received % INBOUND_COUNTER_LOG_EVERY === 0) {
|
|
731
|
-
logInboundCounters(
|
|
745
|
+
logInboundCounters(pluginLog, account.accountId, "periodic");
|
|
732
746
|
}
|
|
733
747
|
return;
|
|
734
748
|
}
|
|
735
749
|
|
|
736
750
|
if (isMessageProcessed(dedupKey)) {
|
|
737
|
-
|
|
751
|
+
pluginLog?.debug?.(`[${account.accountId}] Skipping duplicate message: ${dedupKey}`);
|
|
738
752
|
stats.dedupSkipped += 1;
|
|
739
753
|
acknowledge();
|
|
740
|
-
logInboundCounters(
|
|
754
|
+
logInboundCounters(pluginLog, account.accountId, "dedup-skipped");
|
|
741
755
|
return;
|
|
742
756
|
}
|
|
743
757
|
|
|
744
758
|
const inflightSince = processingDedupKeys.get(dedupKey);
|
|
745
759
|
if (inflightSince !== undefined) {
|
|
746
760
|
if (Date.now() - inflightSince > INFLIGHT_TTL_MS) {
|
|
747
|
-
|
|
761
|
+
pluginLog?.warn?.(
|
|
748
762
|
`[${account.accountId}] Releasing stale in-flight lock for ${dedupKey} (held ${Date.now() - inflightSince}ms > TTL ${INFLIGHT_TTL_MS}ms)`,
|
|
749
763
|
);
|
|
750
764
|
processingDedupKeys.delete(dedupKey);
|
|
751
765
|
} else {
|
|
752
|
-
|
|
766
|
+
pluginLog?.debug?.(
|
|
753
767
|
`[${account.accountId}] Skipping in-flight duplicate message: ${dedupKey}`,
|
|
754
768
|
);
|
|
755
769
|
stats.inflightSkipped += 1;
|
|
756
770
|
acknowledge();
|
|
757
|
-
logInboundCounters(
|
|
771
|
+
logInboundCounters(pluginLog, account.accountId, "inflight-skipped");
|
|
758
772
|
return;
|
|
759
773
|
}
|
|
760
774
|
}
|
|
@@ -767,21 +781,21 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
767
781
|
accountId: account.accountId,
|
|
768
782
|
data,
|
|
769
783
|
sessionWebhook: data.sessionWebhook,
|
|
770
|
-
log:
|
|
784
|
+
log: pluginLog,
|
|
771
785
|
dingtalkConfig: config,
|
|
772
786
|
});
|
|
773
787
|
stats.processed += 1;
|
|
774
788
|
markMessageProcessed(dedupKey);
|
|
775
789
|
if (stats.received % INBOUND_COUNTER_LOG_EVERY === 0) {
|
|
776
|
-
logInboundCounters(
|
|
790
|
+
logInboundCounters(pluginLog, account.accountId, "periodic");
|
|
777
791
|
}
|
|
778
792
|
} finally {
|
|
779
793
|
processingDedupKeys.delete(dedupKey);
|
|
780
794
|
}
|
|
781
795
|
} catch (error: any) {
|
|
782
796
|
stats.failed += 1;
|
|
783
|
-
logInboundCounters(
|
|
784
|
-
|
|
797
|
+
logInboundCounters(pluginLog, account.accountId, "failed");
|
|
798
|
+
pluginLog?.error?.(`[${account.accountId}] Error processing message: ${error.message}`);
|
|
785
799
|
}
|
|
786
800
|
});
|
|
787
801
|
|
|
@@ -794,7 +808,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
794
808
|
try {
|
|
795
809
|
c.socketCallBackResponse(messageId, { success: true });
|
|
796
810
|
} catch (ackError: any) {
|
|
797
|
-
|
|
811
|
+
pluginLog?.warn?.(
|
|
798
812
|
`[${account.accountId}] Failed to acknowledge card callback ${messageId}: ${ackError.message}`,
|
|
799
813
|
);
|
|
800
814
|
}
|
|
@@ -803,7 +817,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
803
817
|
try {
|
|
804
818
|
const payload = JSON.parse(res.data);
|
|
805
819
|
const analysis = analyzeCardCallback(payload);
|
|
806
|
-
|
|
820
|
+
pluginLog?.info?.(
|
|
807
821
|
`[${account.accountId}] [DingTalk][CardCallback] action=${analysis.summary} raw=${JSON.stringify(payload)}`,
|
|
808
822
|
);
|
|
809
823
|
|
|
@@ -826,14 +840,14 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
826
840
|
analysis.feedbackAckText,
|
|
827
841
|
{
|
|
828
842
|
accountId: account.accountId,
|
|
829
|
-
log:
|
|
843
|
+
log: pluginLog,
|
|
830
844
|
},
|
|
831
845
|
);
|
|
832
|
-
|
|
846
|
+
pluginLog?.info?.(
|
|
833
847
|
`[${account.accountId}] [DingTalk][CardCallback] feedback ack sent to ${analysis.feedbackTarget}`,
|
|
834
848
|
);
|
|
835
849
|
} catch (sendErr: any) {
|
|
836
|
-
|
|
850
|
+
pluginLog?.warn?.(
|
|
837
851
|
`[${account.accountId}] [DingTalk][CardCallback] Failed to send feedback ack: ${sendErr?.message || String(sendErr)}`,
|
|
838
852
|
);
|
|
839
853
|
}
|
|
@@ -843,15 +857,15 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
843
857
|
cfg,
|
|
844
858
|
accountId: account.accountId,
|
|
845
859
|
config,
|
|
846
|
-
log:
|
|
860
|
+
log: pluginLog,
|
|
847
861
|
});
|
|
848
862
|
if (!actionResult.handled && analysis.actionId && analysis.actionId !== "feedback_up" && analysis.actionId !== "feedback_down") {
|
|
849
|
-
|
|
863
|
+
pluginLog?.debug?.(
|
|
850
864
|
`[${account.accountId}] [DingTalk][CardCallback] Unhandled actionId=${analysis.actionId}`,
|
|
851
865
|
);
|
|
852
866
|
}
|
|
853
867
|
} catch (error: any) {
|
|
854
|
-
|
|
868
|
+
pluginLog?.error?.(
|
|
855
869
|
`[${account.accountId}] [DingTalk][CardCallback] Failed to parse callback: ${error.message}`,
|
|
856
870
|
);
|
|
857
871
|
} finally {
|
|
@@ -877,15 +891,15 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
877
891
|
return;
|
|
878
892
|
}
|
|
879
893
|
stopped = true;
|
|
880
|
-
|
|
894
|
+
pluginLog?.info?.(`[${account.accountId}] Stopping DingTalk Stream client...`);
|
|
881
895
|
void finalizeActiveCardsForAccount(
|
|
882
896
|
config,
|
|
883
897
|
account.accountId,
|
|
884
898
|
"⚠️ 服务正在重启,当前回复已中断。请重新发送你的问题。",
|
|
885
899
|
accountStorePath,
|
|
886
|
-
|
|
900
|
+
pluginLog,
|
|
887
901
|
).catch((err: any) => {
|
|
888
|
-
|
|
902
|
+
pluginLog?.debug?.(
|
|
889
903
|
`[${account.accountId}] Failed to finalize active cards during stop: ${err.message}`,
|
|
890
904
|
);
|
|
891
905
|
});
|
|
@@ -895,29 +909,35 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
895
909
|
try {
|
|
896
910
|
client.disconnect();
|
|
897
911
|
} catch (err: any) {
|
|
898
|
-
|
|
912
|
+
pluginLog?.warn?.(`[${account.accountId}] Error during disconnect: ${err.message}`);
|
|
899
913
|
}
|
|
900
914
|
nativeStopResolve?.();
|
|
901
915
|
}
|
|
902
916
|
|
|
903
|
-
|
|
904
|
-
...ctx.getStatus(),
|
|
917
|
+
applyStatusPatch({
|
|
905
918
|
running: false,
|
|
919
|
+
connected: false,
|
|
920
|
+
lastEventAt: getCurrentTimestamp(),
|
|
906
921
|
lastStopAt: getCurrentTimestamp(),
|
|
907
922
|
});
|
|
908
923
|
|
|
909
|
-
|
|
924
|
+
pluginLog?.info?.(`[${account.accountId}] DingTalk Stream client stopped`);
|
|
925
|
+
closePluginDebugLog({
|
|
926
|
+
accountId: account.accountId,
|
|
927
|
+
storePath: accountStorePath,
|
|
928
|
+
});
|
|
910
929
|
};
|
|
911
930
|
|
|
912
931
|
if (abortSignal) {
|
|
913
932
|
if (abortSignal.aborted) {
|
|
914
|
-
|
|
933
|
+
pluginLog?.warn?.(
|
|
915
934
|
`[${account.accountId}] Abort signal already active, skipping connection`,
|
|
916
935
|
);
|
|
917
936
|
|
|
918
|
-
|
|
919
|
-
...ctx.getStatus(),
|
|
937
|
+
applyStatusPatch({
|
|
920
938
|
running: false,
|
|
939
|
+
connected: false,
|
|
940
|
+
lastEventAt: getCurrentTimestamp(),
|
|
921
941
|
lastStopAt: getCurrentTimestamp(),
|
|
922
942
|
lastError: "Connection aborted before start",
|
|
923
943
|
});
|
|
@@ -929,7 +949,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
929
949
|
if (stopped) {
|
|
930
950
|
return;
|
|
931
951
|
}
|
|
932
|
-
|
|
952
|
+
pluginLog?.info?.(
|
|
933
953
|
`[${account.accountId}] Abort signal received, stopping DingTalk Stream client...`,
|
|
934
954
|
);
|
|
935
955
|
stopClient();
|
|
@@ -940,17 +960,19 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
940
960
|
try {
|
|
941
961
|
await client.connect();
|
|
942
962
|
if (!stopped) {
|
|
943
|
-
|
|
944
|
-
...ctx.getStatus(),
|
|
963
|
+
applyStatusPatch({
|
|
945
964
|
running: true,
|
|
965
|
+
connected: true,
|
|
966
|
+
lastConnectedAt: getCurrentTimestamp(),
|
|
967
|
+
lastEventAt: getCurrentTimestamp(),
|
|
946
968
|
lastStartAt: getCurrentTimestamp(),
|
|
947
969
|
lastError: null,
|
|
948
970
|
});
|
|
949
|
-
|
|
971
|
+
pluginLog?.info?.(`[${account.accountId}] DingTalk Stream client connected successfully`);
|
|
950
972
|
await nativeStopPromise;
|
|
951
973
|
}
|
|
952
974
|
} catch (err: any) {
|
|
953
|
-
|
|
975
|
+
pluginLog?.error?.(
|
|
954
976
|
formatDingTalkConnectionErrorLog(
|
|
955
977
|
// Use connect.open as base scope; instrumentation can override to connect.websocket
|
|
956
978
|
"connect.open",
|
|
@@ -958,9 +980,10 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
958
980
|
`[${account.accountId}] Failed to establish connection: ${err.message}`,
|
|
959
981
|
) ?? `[${account.accountId}] Failed to establish connection: ${err.message}`,
|
|
960
982
|
);
|
|
961
|
-
|
|
962
|
-
...ctx.getStatus(),
|
|
983
|
+
applyStatusPatch({
|
|
963
984
|
running: false,
|
|
985
|
+
connected: false,
|
|
986
|
+
lastEventAt: getCurrentTimestamp(),
|
|
964
987
|
lastError: err.message || "Connection failed",
|
|
965
988
|
});
|
|
966
989
|
throw err;
|
|
@@ -984,13 +1007,15 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
984
1007
|
if (stopped) {
|
|
985
1008
|
return;
|
|
986
1009
|
}
|
|
987
|
-
|
|
1010
|
+
pluginLog?.debug?.(
|
|
988
1011
|
`[${account.accountId}] Connection state changed to: ${state}${error ? ` (${error})` : ""}`,
|
|
989
1012
|
);
|
|
990
1013
|
if (state === ConnectionState.CONNECTED) {
|
|
991
|
-
|
|
992
|
-
...ctx.getStatus(),
|
|
1014
|
+
applyStatusPatch({
|
|
993
1015
|
running: true,
|
|
1016
|
+
connected: true,
|
|
1017
|
+
lastConnectedAt: getCurrentTimestamp(),
|
|
1018
|
+
lastEventAt: getCurrentTimestamp(),
|
|
994
1019
|
lastStartAt: getCurrentTimestamp(),
|
|
995
1020
|
lastError: null,
|
|
996
1021
|
});
|
|
@@ -1007,20 +1032,21 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
1007
1032
|
}
|
|
1008
1033
|
}
|
|
1009
1034
|
if (cleared > 0) {
|
|
1010
|
-
|
|
1035
|
+
pluginLog?.info?.(
|
|
1011
1036
|
`[${account.accountId}] Cleared ${cleared} stale in-flight lock(s) on disconnect`,
|
|
1012
1037
|
);
|
|
1013
1038
|
}
|
|
1014
|
-
|
|
1015
|
-
...ctx.getStatus(),
|
|
1039
|
+
applyStatusPatch({
|
|
1016
1040
|
running: false,
|
|
1041
|
+
connected: false,
|
|
1042
|
+
lastEventAt: getCurrentTimestamp(),
|
|
1017
1043
|
lastError: error || `Connection ${state.toLowerCase()}`,
|
|
1018
1044
|
});
|
|
1019
1045
|
}
|
|
1020
1046
|
},
|
|
1021
1047
|
};
|
|
1022
1048
|
|
|
1023
|
-
|
|
1049
|
+
pluginLog?.debug?.(
|
|
1024
1050
|
`[${account.accountId}] Connection config: maxAttempts=${connectionConfig.maxAttempts}, ` +
|
|
1025
1051
|
`initialDelay=${connectionConfig.initialDelay}ms, maxDelay=${connectionConfig.maxDelay}ms, ` +
|
|
1026
1052
|
`jitter=${connectionConfig.jitter}`,
|
|
@@ -1030,7 +1056,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
1030
1056
|
client,
|
|
1031
1057
|
account.accountId,
|
|
1032
1058
|
connectionConfig,
|
|
1033
|
-
|
|
1059
|
+
pluginLog,
|
|
1034
1060
|
createStreamClient,
|
|
1035
1061
|
);
|
|
1036
1062
|
|
|
@@ -1038,23 +1064,25 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
1038
1064
|
await connectionManager.connect();
|
|
1039
1065
|
|
|
1040
1066
|
if (!stopped && connectionManager.isConnected()) {
|
|
1041
|
-
|
|
1042
|
-
...ctx.getStatus(),
|
|
1067
|
+
applyStatusPatch({
|
|
1043
1068
|
running: true,
|
|
1069
|
+
connected: true,
|
|
1070
|
+
lastConnectedAt: getCurrentTimestamp(),
|
|
1071
|
+
lastEventAt: getCurrentTimestamp(),
|
|
1044
1072
|
lastStartAt: getCurrentTimestamp(),
|
|
1045
1073
|
lastError: null,
|
|
1046
1074
|
});
|
|
1047
|
-
|
|
1075
|
+
pluginLog?.info?.(`[${account.accountId}] DingTalk Stream client connected successfully`);
|
|
1048
1076
|
|
|
1049
1077
|
await connectionManager.waitForStop();
|
|
1050
1078
|
} else {
|
|
1051
|
-
|
|
1079
|
+
pluginLog?.info?.(
|
|
1052
1080
|
`[${account.accountId}] DingTalk Stream client connect() completed but channel is ` +
|
|
1053
1081
|
`not running (stopped=${stopped}, connected=${connectionManager.isConnected()})`,
|
|
1054
1082
|
);
|
|
1055
1083
|
}
|
|
1056
1084
|
} catch (err: any) {
|
|
1057
|
-
|
|
1085
|
+
pluginLog?.error?.(
|
|
1058
1086
|
formatDingTalkConnectionErrorLog(
|
|
1059
1087
|
// Use connect.open as base scope; instrumentation can override to connect.websocket
|
|
1060
1088
|
"connect.open",
|
|
@@ -1063,9 +1091,10 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
1063
1091
|
) ?? `[${account.accountId}] Failed to establish connection: ${err.message}`,
|
|
1064
1092
|
);
|
|
1065
1093
|
|
|
1066
|
-
|
|
1067
|
-
...ctx.getStatus(),
|
|
1094
|
+
applyStatusPatch({
|
|
1068
1095
|
running: false,
|
|
1096
|
+
connected: false,
|
|
1097
|
+
lastEventAt: getCurrentTimestamp(),
|
|
1069
1098
|
lastError: err.message || "Connection failed",
|
|
1070
1099
|
});
|
|
1071
1100
|
throw err;
|
|
@@ -1082,7 +1111,10 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
1082
1111
|
defaultRuntime: {
|
|
1083
1112
|
accountId: "default",
|
|
1084
1113
|
running: false,
|
|
1114
|
+
connected: false,
|
|
1085
1115
|
lastEventAt: null,
|
|
1116
|
+
lastConnectedAt: null,
|
|
1117
|
+
lastInboundAt: null,
|
|
1086
1118
|
lastStartAt: null,
|
|
1087
1119
|
lastStopAt: null,
|
|
1088
1120
|
lastError: null,
|
|
@@ -1139,7 +1171,10 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
|
|
|
1139
1171
|
configured: account.configured,
|
|
1140
1172
|
clientId: account.config?.clientId ?? null,
|
|
1141
1173
|
running,
|
|
1174
|
+
connected: runtime?.connected ?? snapshot?.connected ?? null,
|
|
1142
1175
|
lastEventAt: running ? getCurrentTimestamp() : persistedLastEventAt,
|
|
1176
|
+
lastConnectedAt: runtime?.lastConnectedAt ?? snapshot?.lastConnectedAt ?? null,
|
|
1177
|
+
lastInboundAt: runtime?.lastInboundAt ?? snapshot?.lastInboundAt ?? null,
|
|
1143
1178
|
lastStartAt: runtime?.lastStartAt ?? snapshot?.lastStartAt ?? null,
|
|
1144
1179
|
lastStopAt: runtime?.lastStopAt ?? snapshot?.lastStopAt ?? null,
|
|
1145
1180
|
lastError: runtime?.lastError ?? snapshot?.lastError ?? null,
|
|
@@ -1,26 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { resolveNativeCommandSessionTargets } from "openclaw/plugin-sdk/command-auth";
|
|
2
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
2
3
|
import { getDingTalkRuntime } from "../runtime";
|
|
3
4
|
import type { Logger } from "../types";
|
|
4
5
|
|
|
5
|
-
/**
|
|
6
|
-
* Local implementation of the same logic as
|
|
7
|
-
* `resolveNativeCommandSessionTargets` from `openclaw/plugin-sdk/command-auth`.
|
|
8
|
-
*
|
|
9
|
-
* Inlined because the CI openclaw package does not yet export that sub-path.
|
|
10
|
-
* Replace with a direct import once the upstream package is updated.
|
|
11
|
-
*/
|
|
12
|
-
function resolveNativeCommandSessionTargets(params: {
|
|
13
|
-
agentId: string;
|
|
14
|
-
sessionPrefix: string;
|
|
15
|
-
userId: string;
|
|
16
|
-
targetSessionKey: string;
|
|
17
|
-
}): { sessionKey: string; commandTargetSessionKey: string } {
|
|
18
|
-
return {
|
|
19
|
-
sessionKey: `agent:${params.agentId}:${params.sessionPrefix}:${params.userId}`,
|
|
20
|
-
commandTargetSessionKey: params.targetSessionKey,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
6
|
/**
|
|
25
7
|
* Dispatch a native targeted `/stop` command through the OpenClaw SDK,
|
|
26
8
|
* replacing the previous self-built Gateway WebSocket `chat.abort` approach.
|
|
@@ -35,8 +17,8 @@ function resolveNativeCommandSessionTargets(params: {
|
|
|
35
17
|
* and executes `abortEmbeddedPiRun` + `clearSessionQueues`.
|
|
36
18
|
*
|
|
37
19
|
* Accesses SDK functions via `getDingTalkRuntime().channel.reply` — the same
|
|
38
|
-
* pattern used by `inbound-handler.ts` —
|
|
39
|
-
*
|
|
20
|
+
* pattern used by `inbound-handler.ts` — while reusing the upstream native
|
|
21
|
+
* command session-target helper exported by the plugin SDK.
|
|
40
22
|
*/
|
|
41
23
|
export async function dispatchDingTalkCardStopCommand(params: {
|
|
42
24
|
cfg: OpenClawConfig;
|