@xmanrui/dsh-im 2.3.0 → 2.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/README.en.md +9 -4
- package/README.md +9 -4
- package/lib/client.js +411 -156
- package/lib/index.js +218 -210
- package/package.json +1 -1
- package/plugin-src/client/channel-card-meta.js +36 -1
- package/plugin-src/client/channels/dingtalk/api.js +2 -0
- package/plugin-src/client/channels/dingtalk/index.js +9 -1
- package/plugin-src/client/channels/feishu/api.js +3 -0
- package/plugin-src/client/channels/feishu/index.js +46 -28
- package/plugin-src/client/channels/feishu/styles.js +26 -1
- package/plugin-src/client/channels/qq/api.js +2 -0
- package/plugin-src/client/channels/qq/index.js +9 -1
- package/plugin-src/client/channels/shared/token-api.js +2 -0
- package/plugin-src/client/channels/shared/token-channel.js +9 -1
- package/plugin-src/client/channels/wecom/api.js +2 -0
- package/plugin-src/client/channels/wecom/index.js +9 -1
- package/plugin-src/client/channels/weixin/api.js +2 -10
- package/plugin-src/client/channels/weixin/index.js +9 -5
- package/plugin-src/client/channels/whatsapp/api.js +2 -0
- package/plugin-src/client/channels/whatsapp/index.js +9 -1
- package/plugin-src/client/i18n.js +31 -25
- package/plugin-src/client/index.js +16 -2
- package/plugin-src/client/last-message-error.js +17 -0
- package/plugin-src/client/styles.js +5 -1
- package/plugin-src/host/channels/feishu/rpc.mjs +3 -0
- package/src/channels/dingtalk/dingtalk-api.mjs +1 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +50 -16
- package/src/channels/dingtalk/dingtalk-card-stream.mjs +2 -2
- package/src/channels/dingtalk/dingtalk-controller.mjs +2 -0
- package/src/channels/feishu/bridge.mjs +148 -70
- package/src/channels/feishu/feishu-cards.mjs +10 -6
- package/src/channels/feishu/feishu-runtime.mjs +0 -1
- package/src/channels/feishu/message-utils.mjs +1 -1
- package/src/channels/feishu/multi-bot-controller.mjs +3 -17
- package/src/channels/feishu/repair-manager.mjs +7 -2
- package/src/channels/qq/qq-bridge.mjs +84 -28
- package/src/channels/qq/qq-controller.mjs +2 -0
- package/src/channels/shared/control-command.mjs +11 -1
- package/src/channels/shared/harness-client.mjs +40 -4
- package/src/channels/shared/i18n-en/feishu.mjs +27 -25
- package/src/channels/shared/i18n-en/shared-a.mjs +78 -0
- package/src/channels/shared/i18n-en/shared-b.mjs +1 -0
- package/src/channels/shared/i18n-en/telegram.mjs +1 -0
- package/src/channels/shared/message-failure.mjs +244 -0
- package/src/channels/shared/semantic/artifact-delivery.mjs +9 -2
- package/src/channels/shared/text-harness-bridge.mjs +66 -60
- package/src/channels/shared/token-bot-controller.mjs +2 -0
- package/src/channels/slack/slack-controller.mjs +2 -0
- package/src/channels/telegram/telegram-runtime.mjs +1 -0
- package/src/channels/wecom/wecom-bridge.mjs +50 -15
- package/src/channels/wecom/wecom-controller.mjs +2 -0
- package/src/channels/weixin/weixin-api.mjs +47 -0
- package/src/channels/weixin/weixin-bridge.mjs +262 -43
- package/src/channels/weixin/weixin-controller.mjs +2 -15
- package/src/channels/weixin/weixin-runtime.mjs +1 -0
- package/src/channels/whatsapp/whatsapp-controller.mjs +2 -0
|
@@ -49,10 +49,17 @@ import {
|
|
|
49
49
|
createDeliveryReceipt,
|
|
50
50
|
providerMessageIdsFor,
|
|
51
51
|
} from '../shared/semantic/delivery.mjs';
|
|
52
|
+
import {
|
|
53
|
+
channelDeliveryFailure,
|
|
54
|
+
clearLastMessageFailure,
|
|
55
|
+
messageFailureText,
|
|
56
|
+
setLastMessageFailure,
|
|
57
|
+
} from '../shared/message-failure.mjs';
|
|
52
58
|
import { t } from '../shared/i18n.mjs';
|
|
53
59
|
|
|
54
60
|
const INTERACTION_RESOLVED_TEXT = () => t('这个问题已在其他客户端处理,无需再次回答。');
|
|
55
|
-
const
|
|
61
|
+
const DEFAULT_TYPING_KEEPALIVE_MS = 5_000;
|
|
62
|
+
const TYPING_RETRY_DELAY_MS = 60_000;
|
|
56
63
|
|
|
57
64
|
const HELP_TEXT = () => [
|
|
58
65
|
t('微信已连接 DeepSeek Harness。'),
|
|
@@ -79,6 +86,7 @@ const HELP_TEXT = () => [
|
|
|
79
86
|
t('/send 提交当前批次'),
|
|
80
87
|
t('/cancel 取消当前批次'),
|
|
81
88
|
t('/status 检查连接状态'),
|
|
89
|
+
t('/version 查看插件版本'),
|
|
82
90
|
t('/help 显示本帮助'),
|
|
83
91
|
].join('\n');
|
|
84
92
|
|
|
@@ -129,16 +137,6 @@ function canClaimInteractionReply(message, pending) {
|
|
|
129
137
|
&& nonEmptyString(extractWeixinText(message));
|
|
130
138
|
}
|
|
131
139
|
|
|
132
|
-
function safeMessageError(error, userMessage = GENERIC_PROCESSING_ERROR()) {
|
|
133
|
-
const diagnostic = imagePromptDiagnostic(error);
|
|
134
|
-
return {
|
|
135
|
-
code: diagnostic?.code ?? 'message-processing-failed',
|
|
136
|
-
reason: diagnostic?.reason ?? 'UNKNOWN',
|
|
137
|
-
message: diagnostic?.userMessage ?? userMessage,
|
|
138
|
-
at: Date.now(),
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
140
|
function artifactFailureText(fileName, error) {
|
|
143
141
|
const name = String(fileName ?? t('结果文件')).replace(/[\r\n]+/g, ' ').trim() || t('结果文件');
|
|
144
142
|
switch (error?.code) {
|
|
@@ -185,6 +183,7 @@ export class WeixinHarnessBridge {
|
|
|
185
183
|
#logger;
|
|
186
184
|
#replyTimeoutMs;
|
|
187
185
|
#maxMessageChars;
|
|
186
|
+
#typingKeepaliveMs;
|
|
188
187
|
#signal;
|
|
189
188
|
#queues = new Map();
|
|
190
189
|
#pendingInteractions = new Map();
|
|
@@ -194,6 +193,14 @@ export class WeixinHarnessBridge {
|
|
|
194
193
|
#commandTasks = new Set();
|
|
195
194
|
#approvals;
|
|
196
195
|
#batchInputs = new BatchInputManager();
|
|
196
|
+
#typingTicket = null;
|
|
197
|
+
#typingTarget = null;
|
|
198
|
+
#typingTimer = null;
|
|
199
|
+
#typingGeneration = 0;
|
|
200
|
+
#typingTail = Promise.resolve();
|
|
201
|
+
#typingClosed = false;
|
|
202
|
+
#typingRetryAt = 0;
|
|
203
|
+
#typingTicketStale = false;
|
|
197
204
|
|
|
198
205
|
constructor({
|
|
199
206
|
api,
|
|
@@ -206,11 +213,15 @@ export class WeixinHarnessBridge {
|
|
|
206
213
|
logger = console,
|
|
207
214
|
replyTimeoutMs = 600_000,
|
|
208
215
|
maxMessageChars = DEFAULT_WEIXIN_MAX_MESSAGE_CHARS,
|
|
216
|
+
typingKeepaliveMs = DEFAULT_TYPING_KEEPALIVE_MS,
|
|
209
217
|
signal,
|
|
210
218
|
}) {
|
|
211
219
|
if (!api || typeof api.sendText !== 'function') throw new TypeError('Weixin API is required');
|
|
212
220
|
if (!baseUrl || !token || !ownerUserId) throw new TypeError('Weixin account credentials are required');
|
|
213
221
|
if (!harness || !state) throw new TypeError('Harness client and state store are required');
|
|
222
|
+
if (!Number.isFinite(typingKeepaliveMs) || typingKeepaliveMs <= 0) {
|
|
223
|
+
throw new TypeError('typingKeepaliveMs must be a positive number');
|
|
224
|
+
}
|
|
214
225
|
this.#api = api;
|
|
215
226
|
this.#baseUrl = baseUrl;
|
|
216
227
|
this.#token = token;
|
|
@@ -221,6 +232,7 @@ export class WeixinHarnessBridge {
|
|
|
221
232
|
this.#logger = logger;
|
|
222
233
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
223
234
|
this.#maxMessageChars = maxMessageChars;
|
|
235
|
+
this.#typingKeepaliveMs = typingKeepaliveMs;
|
|
224
236
|
this.#signal = signal;
|
|
225
237
|
this.#approvals = new HarnessApprovalQueue({ label: 'weixin', logger });
|
|
226
238
|
}
|
|
@@ -267,6 +279,7 @@ export class WeixinHarnessBridge {
|
|
|
267
279
|
return this.#finishBatchResult(
|
|
268
280
|
message,
|
|
269
281
|
messageId,
|
|
282
|
+
key,
|
|
270
283
|
sender,
|
|
271
284
|
contextToken,
|
|
272
285
|
runId,
|
|
@@ -293,9 +306,12 @@ export class WeixinHarnessBridge {
|
|
|
293
306
|
).catch((error) => {
|
|
294
307
|
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
|
295
308
|
this.#status.lastError = error?.message ?? String(error);
|
|
296
|
-
this.#status
|
|
297
|
-
this.#logger.error?.(
|
|
298
|
-
|
|
309
|
+
const failure = setLastMessageFailure(this.#status, error);
|
|
310
|
+
this.#logger.error?.(
|
|
311
|
+
`[dsh-weixin] failed to process a command [${failure.referenceId}]:`,
|
|
312
|
+
error,
|
|
313
|
+
);
|
|
314
|
+
return this.#sendOutOfBand(key, sender, messageFailureText(failure), contextToken, runId)
|
|
299
315
|
.catch(() => undefined);
|
|
300
316
|
}).finally(() => {
|
|
301
317
|
this.#acceptedMessageIds.delete(messageId);
|
|
@@ -385,7 +401,7 @@ export class WeixinHarnessBridge {
|
|
|
385
401
|
return current;
|
|
386
402
|
}
|
|
387
403
|
|
|
388
|
-
#finishBatchResult(message, messageId, sender, contextToken, runId, result) {
|
|
404
|
+
#finishBatchResult(message, messageId, key, sender, contextToken, runId, result) {
|
|
389
405
|
let task;
|
|
390
406
|
task = Promise.resolve().then(async () => {
|
|
391
407
|
if (this.#state.hasSeen(messageId)) return;
|
|
@@ -393,16 +409,18 @@ export class WeixinHarnessBridge {
|
|
|
393
409
|
this.#status.messagesReceived += 1;
|
|
394
410
|
this.#status.lastMessageAt = new Date().toISOString();
|
|
395
411
|
if (result.message) {
|
|
396
|
-
await this.#
|
|
412
|
+
await this.#sendOutOfBand(key, sender, result.message, contextToken, runId);
|
|
397
413
|
}
|
|
398
414
|
this.#status.lastError = null;
|
|
399
|
-
this.#status.lastMessageError = null;
|
|
400
415
|
}).catch(async (error) => {
|
|
401
416
|
if (this.#signal?.aborted) return;
|
|
402
417
|
this.#status.lastError = error?.message ?? String(error);
|
|
403
|
-
this.#status
|
|
404
|
-
this.#logger.error?.(
|
|
405
|
-
|
|
418
|
+
const failure = setLastMessageFailure(this.#status, error);
|
|
419
|
+
this.#logger.error?.(
|
|
420
|
+
`[dsh-weixin] failed to process a batch input message [${failure.referenceId}]:`,
|
|
421
|
+
error,
|
|
422
|
+
);
|
|
423
|
+
await this.#sendOutOfBand(key, sender, messageFailureText(failure), contextToken, runId)
|
|
406
424
|
.catch(() => undefined);
|
|
407
425
|
}).finally(() => {
|
|
408
426
|
this.#acceptedMessageIds.delete(messageId);
|
|
@@ -420,9 +438,15 @@ export class WeixinHarnessBridge {
|
|
|
420
438
|
)),
|
|
421
439
|
...this.#approvalTasks,
|
|
422
440
|
...this.#commandTasks,
|
|
441
|
+
this.#typingTail,
|
|
423
442
|
]);
|
|
424
443
|
}
|
|
425
444
|
|
|
445
|
+
async close() {
|
|
446
|
+
this.#typingClosed = true;
|
|
447
|
+
await this.#stopTyping({ signal: AbortSignal.timeout(5_000) });
|
|
448
|
+
}
|
|
449
|
+
|
|
426
450
|
async #processFastCommand(
|
|
427
451
|
message,
|
|
428
452
|
messageId,
|
|
@@ -453,10 +477,14 @@ export class WeixinHarnessBridge {
|
|
|
453
477
|
]);
|
|
454
478
|
}
|
|
455
479
|
for (const reply of result?.messages ?? [result?.message]) {
|
|
456
|
-
if (reply)
|
|
480
|
+
if (!reply) continue;
|
|
481
|
+
if (result?.stopped) {
|
|
482
|
+
await this.#send(sender, reply, contextToken, runId);
|
|
483
|
+
} else {
|
|
484
|
+
await this.#sendOutOfBand(key, sender, reply, contextToken, runId);
|
|
485
|
+
}
|
|
457
486
|
}
|
|
458
487
|
this.#status.lastError = null;
|
|
459
|
-
this.#status.lastMessageError = null;
|
|
460
488
|
}
|
|
461
489
|
|
|
462
490
|
async #process(message, key, {
|
|
@@ -482,6 +510,7 @@ export class WeixinHarnessBridge {
|
|
|
482
510
|
const contextToken = typeof message.context_token === 'string' ? message.context_token : undefined;
|
|
483
511
|
const runId = typeof message.run_id === 'string' ? message.run_id : undefined;
|
|
484
512
|
let batchSettled = batchSubmission === null;
|
|
513
|
+
let promptRecorded = false;
|
|
485
514
|
try {
|
|
486
515
|
const promptMessage = preparedMessage ?? weixinInboundMessage(message, this.#api);
|
|
487
516
|
const text = promptMessage.content;
|
|
@@ -536,12 +565,15 @@ export class WeixinHarnessBridge {
|
|
|
536
565
|
return;
|
|
537
566
|
}
|
|
538
567
|
|
|
539
|
-
const content = hasImages
|
|
540
|
-
? await promptContentForMessage(promptMessage, { signal: this.#signal })
|
|
541
|
-
: undefined;
|
|
542
568
|
let answer;
|
|
543
569
|
let artifacts = [];
|
|
570
|
+
await this.#startTyping(sender, contextToken);
|
|
544
571
|
try {
|
|
572
|
+
const content = hasImages
|
|
573
|
+
? await promptContentForMessage(promptMessage, { signal: this.#signal })
|
|
574
|
+
: undefined;
|
|
575
|
+
await this.#state.markSeen(messageId);
|
|
576
|
+
promptRecorded = true;
|
|
545
577
|
({ answer, artifacts = [] } = await askInWorkspaceSession({
|
|
546
578
|
harness: this.#harness,
|
|
547
579
|
state: this.#state,
|
|
@@ -553,13 +585,17 @@ export class WeixinHarnessBridge {
|
|
|
553
585
|
timeoutMs: this.#replyTimeoutMs,
|
|
554
586
|
signal: this.#signal,
|
|
555
587
|
control: { owner: this, key },
|
|
588
|
+
onUpdate: () => this.#resumeTyping(key, sender, contextToken),
|
|
556
589
|
onInteraction: (interaction) => this.#handleInteraction(interaction, {
|
|
557
590
|
key,
|
|
558
591
|
actor: sender,
|
|
559
592
|
contextToken,
|
|
560
593
|
runId,
|
|
561
594
|
}),
|
|
562
|
-
onInteractionResolved: (resolution) =>
|
|
595
|
+
onInteractionResolved: async (resolution) => {
|
|
596
|
+
await this.#handleInteractionResolved(resolution);
|
|
597
|
+
await this.#resumeTyping(key, sender, contextToken);
|
|
598
|
+
},
|
|
563
599
|
files: promptMessage.files,
|
|
564
600
|
},
|
|
565
601
|
}));
|
|
@@ -569,6 +605,7 @@ export class WeixinHarnessBridge {
|
|
|
569
605
|
}
|
|
570
606
|
} finally {
|
|
571
607
|
await Promise.allSettled([
|
|
608
|
+
this.#stopTyping(),
|
|
572
609
|
this.#cancelPendingInteraction(key),
|
|
573
610
|
this.#approvals.closeRoute(key),
|
|
574
611
|
]);
|
|
@@ -585,7 +622,7 @@ export class WeixinHarnessBridge {
|
|
|
585
622
|
providerMessageIds: await this.#send(sender, answerText, contextToken, runId),
|
|
586
623
|
});
|
|
587
624
|
} catch (error) {
|
|
588
|
-
textDeliveryError = error;
|
|
625
|
+
textDeliveryError = channelDeliveryFailure(error);
|
|
589
626
|
}
|
|
590
627
|
const delivery = await this.#deliverArtifacts(
|
|
591
628
|
sender,
|
|
@@ -596,11 +633,16 @@ export class WeixinHarnessBridge {
|
|
|
596
633
|
textReceipt,
|
|
597
634
|
);
|
|
598
635
|
if (textDeliveryError && !delivery.userVisible) throw textDeliveryError;
|
|
599
|
-
|
|
636
|
+
if (textDeliveryError && delivery.artifactSendErrors === 0) {
|
|
637
|
+
setLastMessageFailure(this.#status, textDeliveryError);
|
|
638
|
+
}
|
|
639
|
+
if (!promptRecorded) await this.#state.markSeen(messageId);
|
|
600
640
|
this.#status.messagesReplied += 1;
|
|
601
641
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
602
642
|
this.#status.lastError = null;
|
|
603
|
-
|
|
643
|
+
if (!textDeliveryError && delivery.artifactSendErrors === 0) {
|
|
644
|
+
clearLastMessageFailure(this.#status);
|
|
645
|
+
}
|
|
604
646
|
return delivery.receipt;
|
|
605
647
|
} catch (error) {
|
|
606
648
|
let batchFailureMessage = null;
|
|
@@ -613,24 +655,32 @@ export class WeixinHarnessBridge {
|
|
|
613
655
|
batchSettled = true;
|
|
614
656
|
}
|
|
615
657
|
if (error?.code === 'turn-stopped') {
|
|
616
|
-
await this.#state.markSeen(messageId);
|
|
658
|
+
if (!promptRecorded) await this.#state.markSeen(messageId);
|
|
617
659
|
return;
|
|
618
660
|
}
|
|
619
661
|
if (this.#signal?.aborted) return;
|
|
620
662
|
this.#status.lastError = error?.message ?? String(error);
|
|
621
663
|
const userMessage = inboundFileUserMessage(error)
|
|
622
|
-
?? imagePromptUserMessage(error)
|
|
623
|
-
|
|
624
|
-
this.#status
|
|
625
|
-
|
|
664
|
+
?? imagePromptUserMessage(error);
|
|
665
|
+
const imageDiagnostic = imagePromptDiagnostic(error);
|
|
666
|
+
const failure = setLastMessageFailure(this.#status, error, {
|
|
667
|
+
userMessage,
|
|
668
|
+
reason: imageDiagnostic?.reason,
|
|
669
|
+
});
|
|
670
|
+
this.#logger.error?.(
|
|
671
|
+
`[dsh-weixin] failed to process an inbound message [${failure.referenceId}]:`,
|
|
672
|
+
error,
|
|
673
|
+
);
|
|
626
674
|
try {
|
|
627
675
|
await this.#send(
|
|
628
676
|
sender,
|
|
629
|
-
batchFailureMessage
|
|
677
|
+
batchFailureMessage
|
|
678
|
+
? `${messageFailureText(failure)}\n\n${batchFailureMessage}`
|
|
679
|
+
: messageFailureText(failure),
|
|
630
680
|
contextToken,
|
|
631
681
|
runId,
|
|
632
682
|
);
|
|
633
|
-
await this.#state.markSeen(messageId);
|
|
683
|
+
if (!promptRecorded) await this.#state.markSeen(messageId);
|
|
634
684
|
} catch (sendError) {
|
|
635
685
|
this.#logger.error?.('[dsh-weixin] failed to send the safe error reply:', sendError);
|
|
636
686
|
}
|
|
@@ -740,7 +790,6 @@ export class WeixinHarnessBridge {
|
|
|
740
790
|
});
|
|
741
791
|
this.#clearPendingInteraction(key, pending.interactionId);
|
|
742
792
|
this.#status.lastError = null;
|
|
743
|
-
this.#status.lastMessageError = null;
|
|
744
793
|
} catch (error) {
|
|
745
794
|
if (this.#signal?.aborted) return;
|
|
746
795
|
if (error?.code === 'interaction-not-pending') {
|
|
@@ -934,20 +983,24 @@ export class WeixinHarnessBridge {
|
|
|
934
983
|
async #handleInteractionFailure(message, messageId, error) {
|
|
935
984
|
if (this.#signal?.aborted) return;
|
|
936
985
|
this.#status.lastError = error?.message ?? String(error);
|
|
937
|
-
this.#status
|
|
938
|
-
this.#logger.error?.(
|
|
986
|
+
const failure = setLastMessageFailure(this.#status, error);
|
|
987
|
+
this.#logger.error?.(
|
|
988
|
+
`[dsh-weixin] failed to process an interaction reply [${failure.referenceId}]:`,
|
|
989
|
+
error,
|
|
990
|
+
);
|
|
939
991
|
if (!this.#state.hasSeen(messageId)) {
|
|
940
992
|
await this.#state.markSeen(messageId).catch(() => undefined);
|
|
941
993
|
}
|
|
942
994
|
await this.#send(
|
|
943
995
|
nonEmptyString(message?.from_user_id),
|
|
944
|
-
|
|
996
|
+
messageFailureText(failure),
|
|
945
997
|
nonEmptyString(message?.context_token) ?? undefined,
|
|
946
998
|
nonEmptyString(message?.run_id) ?? undefined,
|
|
947
999
|
).catch(() => undefined);
|
|
948
1000
|
}
|
|
949
1001
|
|
|
950
1002
|
async #send(toUserId, text, contextToken, runId) {
|
|
1003
|
+
await this.#stopTyping();
|
|
951
1004
|
const providerMessageIds = [];
|
|
952
1005
|
for (const chunk of splitWeixinText(text, this.#maxMessageChars)) {
|
|
953
1006
|
const result = await this.#api.sendText({
|
|
@@ -964,6 +1017,164 @@ export class WeixinHarnessBridge {
|
|
|
964
1017
|
return providerMessageIds;
|
|
965
1018
|
}
|
|
966
1019
|
|
|
1020
|
+
async #sendOutOfBand(key, toUserId, text, contextToken, runId) {
|
|
1021
|
+
try {
|
|
1022
|
+
return await this.#send(toUserId, text, contextToken, runId);
|
|
1023
|
+
} finally {
|
|
1024
|
+
if (this.#queues.has(key)) {
|
|
1025
|
+
await this.#resumeTyping(key, toUserId, contextToken);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
#queueTyping(operation) {
|
|
1031
|
+
const task = this.#typingTail
|
|
1032
|
+
.catch(() => undefined)
|
|
1033
|
+
.then(operation);
|
|
1034
|
+
this.#typingTail = task.catch(() => undefined);
|
|
1035
|
+
return task;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
async #startTyping(toUserId, contextToken) {
|
|
1039
|
+
const target = nonEmptyString(toUserId);
|
|
1040
|
+
if (!target
|
|
1041
|
+
|| this.#typingClosed
|
|
1042
|
+
|| this.#signal?.aborted
|
|
1043
|
+
|| Date.now() < this.#typingRetryAt
|
|
1044
|
+
|| typeof this.#api.getConfig !== 'function'
|
|
1045
|
+
|| typeof this.#api.sendTyping !== 'function') return false;
|
|
1046
|
+
if (this.#typingTarget === target) return true;
|
|
1047
|
+
|
|
1048
|
+
const generation = ++this.#typingGeneration;
|
|
1049
|
+
try {
|
|
1050
|
+
return await this.#queueTyping(async () => {
|
|
1051
|
+
if (generation !== this.#typingGeneration || this.#typingClosed) return false;
|
|
1052
|
+
let ticket = this.#typingTicket;
|
|
1053
|
+
if (!ticket) {
|
|
1054
|
+
const config = await this.#api.getConfig({
|
|
1055
|
+
baseUrl: this.#baseUrl,
|
|
1056
|
+
token: this.#token,
|
|
1057
|
+
toUserId: target,
|
|
1058
|
+
contextToken,
|
|
1059
|
+
signal: this.#signal,
|
|
1060
|
+
});
|
|
1061
|
+
ticket = nonEmptyString(config?.typingTicket);
|
|
1062
|
+
if (!ticket) {
|
|
1063
|
+
this.#typingRetryAt = Date.now() + TYPING_RETRY_DELAY_MS;
|
|
1064
|
+
return false;
|
|
1065
|
+
}
|
|
1066
|
+
if (generation !== this.#typingGeneration || this.#typingClosed) return false;
|
|
1067
|
+
this.#typingTicket = ticket;
|
|
1068
|
+
this.#typingRetryAt = 0;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
this.#typingTarget = target;
|
|
1072
|
+
await this.#api.sendTyping({
|
|
1073
|
+
baseUrl: this.#baseUrl,
|
|
1074
|
+
token: this.#token,
|
|
1075
|
+
toUserId: target,
|
|
1076
|
+
typingTicket: ticket,
|
|
1077
|
+
status: 1,
|
|
1078
|
+
signal: this.#signal,
|
|
1079
|
+
});
|
|
1080
|
+
if (generation !== this.#typingGeneration || this.#typingClosed) return false;
|
|
1081
|
+
this.#typingTicketStale = false;
|
|
1082
|
+
this.#typingRetryAt = 0;
|
|
1083
|
+
this.#scheduleTyping(generation);
|
|
1084
|
+
return true;
|
|
1085
|
+
});
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
if (generation === this.#typingGeneration) {
|
|
1088
|
+
this.#clearTypingTimer();
|
|
1089
|
+
this.#typingTicketStale = Boolean(this.#typingTarget && this.#typingTicket);
|
|
1090
|
+
this.#typingRetryAt = Date.now() + TYPING_RETRY_DELAY_MS;
|
|
1091
|
+
}
|
|
1092
|
+
if (!this.#signal?.aborted && !this.#typingClosed) {
|
|
1093
|
+
this.#logger.warn?.('[dsh-weixin] typing indicator failed:', error);
|
|
1094
|
+
}
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
async #stopTyping({ signal = AbortSignal.timeout(5_000) } = {}) {
|
|
1100
|
+
++this.#typingGeneration;
|
|
1101
|
+
this.#clearTypingTimer();
|
|
1102
|
+
const target = this.#typingTarget;
|
|
1103
|
+
const ticket = this.#typingTicket;
|
|
1104
|
+
const stale = this.#typingTicketStale;
|
|
1105
|
+
this.#typingTarget = null;
|
|
1106
|
+
if (typeof this.#api.sendTyping !== 'function') return false;
|
|
1107
|
+
try {
|
|
1108
|
+
return await this.#queueTyping(async () => {
|
|
1109
|
+
if (!target || !ticket) return false;
|
|
1110
|
+
try {
|
|
1111
|
+
await this.#api.sendTyping({
|
|
1112
|
+
baseUrl: this.#baseUrl,
|
|
1113
|
+
token: this.#token,
|
|
1114
|
+
toUserId: target,
|
|
1115
|
+
typingTicket: ticket,
|
|
1116
|
+
status: 2,
|
|
1117
|
+
signal,
|
|
1118
|
+
});
|
|
1119
|
+
} finally {
|
|
1120
|
+
if (stale && this.#typingTicket === ticket) this.#typingTicket = null;
|
|
1121
|
+
this.#typingTicketStale = false;
|
|
1122
|
+
}
|
|
1123
|
+
return true;
|
|
1124
|
+
});
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
if (!signal?.aborted) {
|
|
1127
|
+
this.#logger.warn?.('[dsh-weixin] typing cancellation failed:', error);
|
|
1128
|
+
}
|
|
1129
|
+
return false;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
#scheduleTyping(generation) {
|
|
1134
|
+
this.#clearTypingTimer();
|
|
1135
|
+
if (generation !== this.#typingGeneration || !this.#typingTarget || this.#typingClosed) return;
|
|
1136
|
+
const timer = setTimeout(() => {
|
|
1137
|
+
if (this.#typingTimer === timer) this.#typingTimer = null;
|
|
1138
|
+
void this.#queueTyping(async () => {
|
|
1139
|
+
if (generation !== this.#typingGeneration || !this.#typingTarget || this.#typingClosed) {
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
await this.#api.sendTyping({
|
|
1143
|
+
baseUrl: this.#baseUrl,
|
|
1144
|
+
token: this.#token,
|
|
1145
|
+
toUserId: this.#typingTarget,
|
|
1146
|
+
typingTicket: this.#typingTicket,
|
|
1147
|
+
status: 1,
|
|
1148
|
+
signal: this.#signal,
|
|
1149
|
+
});
|
|
1150
|
+
return true;
|
|
1151
|
+
}).then((sent) => {
|
|
1152
|
+
if (sent) this.#scheduleTyping(generation);
|
|
1153
|
+
}).catch((error) => {
|
|
1154
|
+
if (generation !== this.#typingGeneration) return;
|
|
1155
|
+
this.#typingTicketStale = Boolean(this.#typingTarget && this.#typingTicket);
|
|
1156
|
+
this.#typingRetryAt = Date.now() + TYPING_RETRY_DELAY_MS;
|
|
1157
|
+
if (!this.#signal?.aborted && !this.#typingClosed) {
|
|
1158
|
+
this.#logger.warn?.('[dsh-weixin] typing keepalive failed:', error);
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
}, this.#typingKeepaliveMs);
|
|
1162
|
+
timer.unref?.();
|
|
1163
|
+
this.#typingTimer = timer;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
#clearTypingTimer() {
|
|
1167
|
+
if (this.#typingTimer) clearTimeout(this.#typingTimer);
|
|
1168
|
+
this.#typingTimer = null;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
#resumeTyping(key, toUserId, contextToken) {
|
|
1172
|
+
if (this.#pendingInteractions.has(key) || this.#approvals.hasPending(key)) {
|
|
1173
|
+
return Promise.resolve(false);
|
|
1174
|
+
}
|
|
1175
|
+
return this.#startTyping(toUserId, contextToken);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
967
1178
|
async #deliverArtifacts(toUserId, replyTo, artifacts, contextToken, runId, baseReceipt) {
|
|
968
1179
|
const sendArtifact = (method, file) => this.#api[method]({
|
|
969
1180
|
baseUrl: this.#baseUrl,
|
|
@@ -987,9 +1198,13 @@ export class WeixinHarnessBridge {
|
|
|
987
1198
|
sendFile: typeof this.#api.sendFile === 'function'
|
|
988
1199
|
? (file) => sendArtifact('sendFile', file)
|
|
989
1200
|
: undefined,
|
|
990
|
-
|
|
1201
|
+
onFailure: (artifact, error) => setLastMessageFailure(this.#status, error, {
|
|
1202
|
+
userMessage: artifactFailureText(artifact?.fileName, error),
|
|
1203
|
+
reason: error?.code,
|
|
1204
|
+
}),
|
|
1205
|
+
sendFailureNotice: (_artifact, _error, failure) => this.#send(
|
|
991
1206
|
toUserId,
|
|
992
|
-
|
|
1207
|
+
messageFailureText(failure),
|
|
993
1208
|
contextToken,
|
|
994
1209
|
runId,
|
|
995
1210
|
),
|
|
@@ -999,6 +1214,10 @@ export class WeixinHarnessBridge {
|
|
|
999
1214
|
+ delivery.artifactsSent;
|
|
1000
1215
|
this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0)
|
|
1001
1216
|
+ delivery.artifactSendErrors;
|
|
1002
|
-
return {
|
|
1217
|
+
return {
|
|
1218
|
+
receipt: delivery.receipt,
|
|
1219
|
+
userVisible: delivery.userVisible,
|
|
1220
|
+
artifactSendErrors: delivery.artifactSendErrors,
|
|
1221
|
+
};
|
|
1003
1222
|
}
|
|
1004
1223
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
connectionTestMessage,
|
|
11
11
|
connectionTestTargetUnavailable,
|
|
12
12
|
} from '../shared/connection-test.mjs';
|
|
13
|
+
import { publicMessageFailure } from '../shared/message-failure.mjs';
|
|
13
14
|
import { t } from '../shared/i18n.mjs';
|
|
14
15
|
|
|
15
16
|
const ACTIVE_ATTEMPT_STATES = new Set([
|
|
@@ -74,20 +75,6 @@ function safeAccountError(code, message) {
|
|
|
74
75
|
return Object.freeze({ code, message });
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
function publicMessageError(value) {
|
|
78
|
-
if (!value || typeof value !== 'object'
|
|
79
|
-
|| typeof value.code !== 'string' || !value.code
|
|
80
|
-
|| typeof value.reason !== 'string' || !value.reason
|
|
81
|
-
|| typeof value.message !== 'string' || !value.message
|
|
82
|
-
|| !Number.isFinite(value.at)) return null;
|
|
83
|
-
return {
|
|
84
|
-
code: value.code.slice(0, 64),
|
|
85
|
-
reason: value.reason.slice(0, 128),
|
|
86
|
-
message: value.message.slice(0, 500),
|
|
87
|
-
at: value.at,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
78
|
function activationStageError(code, cause) {
|
|
92
79
|
const error = new Error(`Weixin activation failed during ${code}`, { cause });
|
|
93
80
|
error.name = 'WeixinActivationStageError';
|
|
@@ -377,7 +364,7 @@ export class WeixinController {
|
|
|
377
364
|
messagesReceived: runtimeStatus?.messagesReceived ?? 0,
|
|
378
365
|
messagesReplied: runtimeStatus?.messagesReplied ?? 0,
|
|
379
366
|
},
|
|
380
|
-
lastMessageError:
|
|
367
|
+
lastMessageError: publicMessageFailure(runtimeStatus?.lastMessageError),
|
|
381
368
|
error: error ? structuredClone(error) : null,
|
|
382
369
|
};
|
|
383
370
|
});
|
|
@@ -285,6 +285,7 @@ export class WeixinRuntime {
|
|
|
285
285
|
this.#abortController?.abort();
|
|
286
286
|
this.#abortController = null;
|
|
287
287
|
this.#monitor = null;
|
|
288
|
+
await bridge?.close?.();
|
|
288
289
|
await monitor?.catch(() => undefined);
|
|
289
290
|
await bridge?.waitForIdle();
|
|
290
291
|
this.#bridge = null;
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
|
|
3
3
|
import { connectionTestMessage } from '../shared/connection-test.mjs';
|
|
4
4
|
import { t } from '../shared/i18n.mjs';
|
|
5
|
+
import { publicMessageFailure } from '../shared/message-failure.mjs';
|
|
5
6
|
import {
|
|
6
7
|
deriveWhatsappBotId,
|
|
7
8
|
maskWhatsappAccount,
|
|
@@ -285,6 +286,7 @@ export class WhatsappController {
|
|
|
285
286
|
messagesReceived: runtimeStatus?.messagesReceived ?? 0,
|
|
286
287
|
messagesReplied: runtimeStatus?.messagesReplied ?? 0,
|
|
287
288
|
},
|
|
289
|
+
lastMessageError: publicMessageFailure(runtimeStatus?.lastMessageError),
|
|
288
290
|
accessPolicy: normalizeWhatsappAccessPolicy(config),
|
|
289
291
|
error: structuredClone(this.#errors.get(config.botId) ?? null),
|
|
290
292
|
};
|