@xmanrui/dsh-im 2.2.0 → 2.3.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 +6 -2
- package/README.md +6 -2
- package/lib/client.js +2 -2
- package/lib/index.js +217 -189
- package/package.json +1 -1
- package/plugin-src/client/channels/whatsapp/index.js +1 -1
- package/plugin-src/client/i18n.js +1 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +95 -3
- package/src/channels/discord/discord-runtime.mjs +3 -0
- package/src/channels/feishu/bridge.mjs +127 -4
- package/src/channels/feishu/feishu-cards.mjs +8 -0
- package/src/channels/qq/qq-bridge.mjs +98 -8
- package/src/channels/shared/batch-input.mjs +209 -0
- package/src/channels/shared/i18n-en/feishu.mjs +3 -2
- package/src/channels/shared/i18n-en/shared-c.mjs +49 -0
- package/src/channels/shared/text-harness-bridge.mjs +111 -2
- package/src/channels/slack/slack-runtime.mjs +1 -0
- package/src/channels/telegram/telegram-runtime.mjs +4 -0
- package/src/channels/wecom/wecom-bridge.mjs +101 -4
- package/src/channels/weixin/weixin-bridge.mjs +100 -3
- package/src/channels/weixin/weixin-runtime.mjs +18 -1
- package/src/channels/whatsapp/whatsapp-runtime.mjs +29 -6
|
@@ -5,6 +5,12 @@ import {
|
|
|
5
5
|
validHarnessQuestion,
|
|
6
6
|
} from '../shared/harness-question.mjs';
|
|
7
7
|
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
8
|
+
import {
|
|
9
|
+
BatchInputManager,
|
|
10
|
+
batchInputBusyMessage,
|
|
11
|
+
batchInputGroupUnsupportedMessage,
|
|
12
|
+
isBatchInputCommand,
|
|
13
|
+
} from '../shared/batch-input.mjs';
|
|
8
14
|
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
9
15
|
import {
|
|
10
16
|
isControlCommand,
|
|
@@ -63,6 +69,9 @@ function helpText() {
|
|
|
63
69
|
t('/preset --default 跟随 Host 默认'),
|
|
64
70
|
t('/stop 停止当前任务'),
|
|
65
71
|
t('/steer 补充指令 纠偏当前任务'),
|
|
72
|
+
t('/batch 开始批量输入(仅私聊,最多 10 条文字)'),
|
|
73
|
+
t('/send 提交当前批次'),
|
|
74
|
+
t('/cancel 取消当前批次'),
|
|
66
75
|
t('/status 检查连接状态'),
|
|
67
76
|
t('/help 显示本帮助'),
|
|
68
77
|
].join('\n');
|
|
@@ -254,6 +263,10 @@ function interactionReplyText(frame) {
|
|
|
254
263
|
return bodyOf(frame).msgtype === 'text' ? messageText(frame) : '';
|
|
255
264
|
}
|
|
256
265
|
|
|
266
|
+
function isNativeWecomText(frame) {
|
|
267
|
+
return bodyOf(frame).msgtype === 'text' && Boolean(nonEmptyString(messageText(frame)));
|
|
268
|
+
}
|
|
269
|
+
|
|
257
270
|
function splitUtf8(text, maxBytes = MAX_REPLY_BYTES) {
|
|
258
271
|
const source = String(text ?? '').trim();
|
|
259
272
|
if (!source) return [];
|
|
@@ -466,6 +479,7 @@ export class WecomHarnessBridge {
|
|
|
466
479
|
#approvalTasks = new Set();
|
|
467
480
|
#commandTasks = new Set();
|
|
468
481
|
#approvals;
|
|
482
|
+
#batchInputs = new BatchInputManager();
|
|
469
483
|
#prefetchedImageCount = 0;
|
|
470
484
|
|
|
471
485
|
constructor({
|
|
@@ -523,6 +537,40 @@ export class WecomHarnessBridge {
|
|
|
523
537
|
const pending = this.#pendingInteractions.get(key);
|
|
524
538
|
const commandMessage = wecomInboundMessage(frame, this.#client);
|
|
525
539
|
const commandText = nonEmptyString(commandMessage.content) ?? '';
|
|
540
|
+
const batchCommand = isBatchInputCommand(commandText);
|
|
541
|
+
const batchStatus = this.#batchInputs.status(key);
|
|
542
|
+
if (batchCommand && body.chattype === 'group') {
|
|
543
|
+
return this.#finishBatchResult(
|
|
544
|
+
frame,
|
|
545
|
+
messageId,
|
|
546
|
+
chatId,
|
|
547
|
+
{ message: batchInputGroupUnsupportedMessage() },
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
if (body.chattype === 'single'
|
|
551
|
+
&& (batchCommand || batchStatus.phase === 'collecting')) {
|
|
552
|
+
const exactBatchStart = /^\/batch$/iu.test(commandText);
|
|
553
|
+
const result = exactBatchStart
|
|
554
|
+
&& batchStatus.phase === 'idle'
|
|
555
|
+
&& (this.#queues.has(key) || pending || this.#approvals.hasPending(key))
|
|
556
|
+
? { handled: true, kind: 'busy', message: batchInputBusyMessage() }
|
|
557
|
+
: this.#batchInputs.handle(key, commandText, {
|
|
558
|
+
plainText: isNativeWecomText(frame),
|
|
559
|
+
});
|
|
560
|
+
if (result.handled) {
|
|
561
|
+
if (result.kind === 'submit') {
|
|
562
|
+
return this.#enqueueMessage({
|
|
563
|
+
...frame,
|
|
564
|
+
body: {
|
|
565
|
+
...body,
|
|
566
|
+
msgtype: 'text',
|
|
567
|
+
text: { content: result.prompt },
|
|
568
|
+
},
|
|
569
|
+
}, messageId, key, { batchSubmission: result });
|
|
570
|
+
}
|
|
571
|
+
return this.#finishBatchResult(frame, messageId, chatId, result);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
526
574
|
const commandRunner = hasInboundFiles(commandMessage) ? null : isControlCommand(commandText)
|
|
527
575
|
? runControlCommand
|
|
528
576
|
: (isModelCommand(commandText)
|
|
@@ -616,6 +664,7 @@ export class WecomHarnessBridge {
|
|
|
616
664
|
#enqueueMessage(frame, messageId, key, {
|
|
617
665
|
releaseMessageId = true,
|
|
618
666
|
alreadyRecorded = false,
|
|
667
|
+
batchSubmission = null,
|
|
619
668
|
} = {}) {
|
|
620
669
|
// WeCom image URLs expire after five minutes, while a conversation turn
|
|
621
670
|
// may legally stay queued longer. Start the authenticated SDK download as
|
|
@@ -637,7 +686,11 @@ export class WecomHarnessBridge {
|
|
|
637
686
|
const previous = this.#queues.get(key) ?? Promise.resolve();
|
|
638
687
|
const current = previous
|
|
639
688
|
.catch(() => undefined)
|
|
640
|
-
.then(() => this.#process(frame, {
|
|
689
|
+
.then(() => this.#process(frame, {
|
|
690
|
+
alreadyRecorded,
|
|
691
|
+
preparedMessage,
|
|
692
|
+
batchSubmission,
|
|
693
|
+
}))
|
|
641
694
|
.finally(() => {
|
|
642
695
|
this.#prefetchedImageCount -= reservedImages;
|
|
643
696
|
if (releaseMessageId) this.#acceptedMessageIds.delete(messageId);
|
|
@@ -647,6 +700,29 @@ export class WecomHarnessBridge {
|
|
|
647
700
|
return current;
|
|
648
701
|
}
|
|
649
702
|
|
|
703
|
+
#finishBatchResult(frame, messageId, chatId, result) {
|
|
704
|
+
let task;
|
|
705
|
+
task = Promise.resolve().then(async () => {
|
|
706
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
707
|
+
await this.#state.markSeen(messageId);
|
|
708
|
+
this.#status.messagesReceived += 1;
|
|
709
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
710
|
+
if (result.message) await this.#sendImmediate(frame, chatId, result.message);
|
|
711
|
+
this.#status.lastError = null;
|
|
712
|
+
}).catch(async (error) => {
|
|
713
|
+
if (this.#signal?.aborted) return;
|
|
714
|
+
this.#status.lastError = error?.message ?? String(error);
|
|
715
|
+
this.#logger.error?.('[dsh-im:wecom] failed to process a batch input message');
|
|
716
|
+
await this.#sendImmediate(frame, chatId, t('消息处理失败,请稍后重试。'))
|
|
717
|
+
.catch(() => undefined);
|
|
718
|
+
}).finally(() => {
|
|
719
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
720
|
+
this.#commandTasks.delete(task);
|
|
721
|
+
});
|
|
722
|
+
this.#commandTasks.add(task);
|
|
723
|
+
return task;
|
|
724
|
+
}
|
|
725
|
+
|
|
650
726
|
async waitForIdle() {
|
|
651
727
|
await Promise.allSettled([
|
|
652
728
|
...this.#queues.values(),
|
|
@@ -748,7 +824,11 @@ export class WecomHarnessBridge {
|
|
|
748
824
|
};
|
|
749
825
|
}
|
|
750
826
|
|
|
751
|
-
async #process(frame, {
|
|
827
|
+
async #process(frame, {
|
|
828
|
+
alreadyRecorded = false,
|
|
829
|
+
preparedMessage,
|
|
830
|
+
batchSubmission = null,
|
|
831
|
+
} = {}) {
|
|
752
832
|
if (this.#signal?.aborted) return;
|
|
753
833
|
const body = bodyOf(frame);
|
|
754
834
|
const messageId = typeof body.msgid === 'string' ? body.msgid : '';
|
|
@@ -767,6 +847,7 @@ export class WecomHarnessBridge {
|
|
|
767
847
|
const key = conversationKey(frame);
|
|
768
848
|
let streamId = null;
|
|
769
849
|
let streamStarted = false;
|
|
850
|
+
let batchSettled = batchSubmission === null;
|
|
770
851
|
try {
|
|
771
852
|
if (!text && !hasImages && !hasFiles) {
|
|
772
853
|
await this.#sendImmediate(frame, chatId, t('目前支持文字、图片、文件和语音转写消息。'));
|
|
@@ -855,6 +936,10 @@ export class WecomHarnessBridge {
|
|
|
855
936
|
files: message.files,
|
|
856
937
|
},
|
|
857
938
|
});
|
|
939
|
+
if (batchSubmission) {
|
|
940
|
+
this.#batchInputs.complete(key, batchSubmission.token);
|
|
941
|
+
batchSettled = true;
|
|
942
|
+
}
|
|
858
943
|
|
|
859
944
|
this.#signal?.throwIfAborted();
|
|
860
945
|
const displayAnswer = answerTextForDelivery(answer, artifacts);
|
|
@@ -915,6 +1000,15 @@ export class WecomHarnessBridge {
|
|
|
915
1000
|
this.#status.lastError = null;
|
|
916
1001
|
return delivery.receipt;
|
|
917
1002
|
} catch (error) {
|
|
1003
|
+
let batchFailureMessage = null;
|
|
1004
|
+
if (!batchSettled && batchSubmission) {
|
|
1005
|
+
if (error?.code === 'turn-stopped') {
|
|
1006
|
+
this.#batchInputs.complete(key, batchSubmission.token);
|
|
1007
|
+
} else {
|
|
1008
|
+
batchFailureMessage = this.#batchInputs.fail(key, batchSubmission.token).message ?? null;
|
|
1009
|
+
}
|
|
1010
|
+
batchSettled = true;
|
|
1011
|
+
}
|
|
918
1012
|
if (error?.code === 'turn-stopped') {
|
|
919
1013
|
if (streamStarted && streamId) {
|
|
920
1014
|
await this.#client.replyStream(frame, streamId, t('已停止。'), true)
|
|
@@ -929,11 +1023,14 @@ export class WecomHarnessBridge {
|
|
|
929
1023
|
const errorText = inboundFileUserMessage(error)
|
|
930
1024
|
?? imagePromptUserMessage(error)
|
|
931
1025
|
?? t('消息处理失败,请稍后重试。');
|
|
1026
|
+
const visibleError = batchFailureMessage
|
|
1027
|
+
? `${errorText}\n\n${batchFailureMessage}`
|
|
1028
|
+
: errorText;
|
|
932
1029
|
try {
|
|
933
1030
|
if (streamStarted && streamId) {
|
|
934
|
-
await this.#client.replyStream(frame, streamId,
|
|
1031
|
+
await this.#client.replyStream(frame, streamId, visibleError, true);
|
|
935
1032
|
} else {
|
|
936
|
-
await this.#sendImmediate(frame, chatId,
|
|
1033
|
+
await this.#sendImmediate(frame, chatId, visibleError);
|
|
937
1034
|
}
|
|
938
1035
|
await this.#state.markSeen(messageId);
|
|
939
1036
|
} catch {
|
|
@@ -12,6 +12,11 @@ import {
|
|
|
12
12
|
validHarnessQuestion,
|
|
13
13
|
} from '../shared/harness-question.mjs';
|
|
14
14
|
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
15
|
+
import {
|
|
16
|
+
BatchInputManager,
|
|
17
|
+
batchInputBusyMessage,
|
|
18
|
+
isBatchInputCommand,
|
|
19
|
+
} from '../shared/batch-input.mjs';
|
|
15
20
|
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
16
21
|
import {
|
|
17
22
|
isControlCommand,
|
|
@@ -70,6 +75,9 @@ const HELP_TEXT = () => [
|
|
|
70
75
|
t('/preset --default 跟随 Host 默认'),
|
|
71
76
|
t('/stop 停止当前任务'),
|
|
72
77
|
t('/steer 补充指令 纠偏当前任务'),
|
|
78
|
+
t('/batch 开始批量输入(仅私聊,最多 10 条文字)'),
|
|
79
|
+
t('/send 提交当前批次'),
|
|
80
|
+
t('/cancel 取消当前批次'),
|
|
73
81
|
t('/status 检查连接状态'),
|
|
74
82
|
t('/help 显示本帮助'),
|
|
75
83
|
].join('\n');
|
|
@@ -104,6 +112,15 @@ function hasWeixinFileItems(message) {
|
|
|
104
112
|
&& message.item_list.some((item) => item?.file_item && typeof item.file_item === 'object');
|
|
105
113
|
}
|
|
106
114
|
|
|
115
|
+
function isNativeWeixinText(message) {
|
|
116
|
+
return Array.isArray(message?.item_list)
|
|
117
|
+
&& message.item_list.length > 0
|
|
118
|
+
&& message.item_list.every((item) => (
|
|
119
|
+
item?.type === 1 && typeof item.text_item?.text === 'string'
|
|
120
|
+
))
|
|
121
|
+
&& Boolean(nonEmptyString(extractWeixinText(message)));
|
|
122
|
+
}
|
|
123
|
+
|
|
107
124
|
function canClaimInteractionReply(message, pending) {
|
|
108
125
|
return pending.questions[pending.index]
|
|
109
126
|
&& nonEmptyString(message?.from_user_id) === pending.actor
|
|
@@ -176,6 +193,7 @@ export class WeixinHarnessBridge {
|
|
|
176
193
|
#approvalTasks = new Set();
|
|
177
194
|
#commandTasks = new Set();
|
|
178
195
|
#approvals;
|
|
196
|
+
#batchInputs = new BatchInputManager();
|
|
179
197
|
|
|
180
198
|
constructor({
|
|
181
199
|
api,
|
|
@@ -227,6 +245,35 @@ export class WeixinHarnessBridge {
|
|
|
227
245
|
const runId = nonEmptyString(message?.run_id) ?? undefined;
|
|
228
246
|
const pending = this.#pendingInteractions.get(key);
|
|
229
247
|
const commandText = nonEmptyString(extractWeixinText(message)) ?? '';
|
|
248
|
+
const batchCommand = isBatchInputCommand(commandText);
|
|
249
|
+
const batchStatus = this.#batchInputs.status(key);
|
|
250
|
+
if (sender === this.#ownerUserId
|
|
251
|
+
&& (batchCommand || batchStatus.phase === 'collecting')) {
|
|
252
|
+
const exactBatchStart = /^\/batch$/iu.test(commandText);
|
|
253
|
+
const result = exactBatchStart
|
|
254
|
+
&& batchStatus.phase === 'idle'
|
|
255
|
+
&& (this.#queues.has(key) || pending || this.#approvals.hasPending(key))
|
|
256
|
+
? { handled: true, kind: 'busy', message: batchInputBusyMessage() }
|
|
257
|
+
: this.#batchInputs.handle(key, commandText, {
|
|
258
|
+
plainText: isNativeWeixinText(message),
|
|
259
|
+
});
|
|
260
|
+
if (result.handled) {
|
|
261
|
+
if (result.kind === 'submit') {
|
|
262
|
+
return this.#enqueueMessage({
|
|
263
|
+
...message,
|
|
264
|
+
item_list: [{ type: 1, text_item: { text: result.prompt } }],
|
|
265
|
+
}, messageId, key, { batchSubmission: result });
|
|
266
|
+
}
|
|
267
|
+
return this.#finishBatchResult(
|
|
268
|
+
message,
|
|
269
|
+
messageId,
|
|
270
|
+
sender,
|
|
271
|
+
contextToken,
|
|
272
|
+
runId,
|
|
273
|
+
result,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
230
277
|
const commandRunner = hasWeixinFileItems(message) ? null : isControlCommand(commandText)
|
|
231
278
|
? runControlCommand
|
|
232
279
|
: (isModelCommand(commandText)
|
|
@@ -314,6 +361,7 @@ export class WeixinHarnessBridge {
|
|
|
314
361
|
#enqueueMessage(message, messageId, key, {
|
|
315
362
|
releaseMessageId = true,
|
|
316
363
|
alreadyRecorded = false,
|
|
364
|
+
batchSubmission = null,
|
|
317
365
|
} = {}) {
|
|
318
366
|
const preparedMessage = message.from_user_id === this.#ownerUserId
|
|
319
367
|
? prefetchInboundFiles(
|
|
@@ -324,7 +372,11 @@ export class WeixinHarnessBridge {
|
|
|
324
372
|
const previous = this.#queues.get(key) ?? Promise.resolve();
|
|
325
373
|
const current = previous
|
|
326
374
|
.catch(() => undefined)
|
|
327
|
-
.then(() => this.#process(message, key, {
|
|
375
|
+
.then(() => this.#process(message, key, {
|
|
376
|
+
alreadyRecorded,
|
|
377
|
+
preparedMessage,
|
|
378
|
+
batchSubmission,
|
|
379
|
+
}))
|
|
328
380
|
.finally(() => {
|
|
329
381
|
if (releaseMessageId) this.#acceptedMessageIds.delete(messageId);
|
|
330
382
|
if (this.#queues.get(key) === current) this.#queues.delete(key);
|
|
@@ -333,6 +385,33 @@ export class WeixinHarnessBridge {
|
|
|
333
385
|
return current;
|
|
334
386
|
}
|
|
335
387
|
|
|
388
|
+
#finishBatchResult(message, messageId, sender, contextToken, runId, result) {
|
|
389
|
+
let task;
|
|
390
|
+
task = Promise.resolve().then(async () => {
|
|
391
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
392
|
+
await this.#state.markSeen(messageId);
|
|
393
|
+
this.#status.messagesReceived += 1;
|
|
394
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
395
|
+
if (result.message) {
|
|
396
|
+
await this.#send(sender, result.message, contextToken, runId);
|
|
397
|
+
}
|
|
398
|
+
this.#status.lastError = null;
|
|
399
|
+
this.#status.lastMessageError = null;
|
|
400
|
+
}).catch(async (error) => {
|
|
401
|
+
if (this.#signal?.aborted) return;
|
|
402
|
+
this.#status.lastError = error?.message ?? String(error);
|
|
403
|
+
this.#status.lastMessageError = safeMessageError(error);
|
|
404
|
+
this.#logger.error?.('[dsh-weixin] failed to process a batch input message:', error);
|
|
405
|
+
await this.#send(sender, GENERIC_PROCESSING_ERROR(), contextToken, runId)
|
|
406
|
+
.catch(() => undefined);
|
|
407
|
+
}).finally(() => {
|
|
408
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
409
|
+
this.#commandTasks.delete(task);
|
|
410
|
+
});
|
|
411
|
+
this.#commandTasks.add(task);
|
|
412
|
+
return task;
|
|
413
|
+
}
|
|
414
|
+
|
|
336
415
|
async waitForIdle() {
|
|
337
416
|
await Promise.allSettled([
|
|
338
417
|
...this.#queues.values(),
|
|
@@ -380,7 +459,11 @@ export class WeixinHarnessBridge {
|
|
|
380
459
|
this.#status.lastMessageError = null;
|
|
381
460
|
}
|
|
382
461
|
|
|
383
|
-
async #process(message, key, {
|
|
462
|
+
async #process(message, key, {
|
|
463
|
+
alreadyRecorded = false,
|
|
464
|
+
preparedMessage,
|
|
465
|
+
batchSubmission = null,
|
|
466
|
+
} = {}) {
|
|
384
467
|
this.#signal?.throwIfAborted();
|
|
385
468
|
const messageId = weixinMessageId(message);
|
|
386
469
|
const sender = nonEmptyString(message?.from_user_id);
|
|
@@ -398,6 +481,7 @@ export class WeixinHarnessBridge {
|
|
|
398
481
|
|
|
399
482
|
const contextToken = typeof message.context_token === 'string' ? message.context_token : undefined;
|
|
400
483
|
const runId = typeof message.run_id === 'string' ? message.run_id : undefined;
|
|
484
|
+
let batchSettled = batchSubmission === null;
|
|
401
485
|
try {
|
|
402
486
|
const promptMessage = preparedMessage ?? weixinInboundMessage(message, this.#api);
|
|
403
487
|
const text = promptMessage.content;
|
|
@@ -479,6 +563,10 @@ export class WeixinHarnessBridge {
|
|
|
479
563
|
files: promptMessage.files,
|
|
480
564
|
},
|
|
481
565
|
}));
|
|
566
|
+
if (batchSubmission) {
|
|
567
|
+
this.#batchInputs.complete(key, batchSubmission.token);
|
|
568
|
+
batchSettled = true;
|
|
569
|
+
}
|
|
482
570
|
} finally {
|
|
483
571
|
await Promise.allSettled([
|
|
484
572
|
this.#cancelPendingInteraction(key),
|
|
@@ -515,6 +603,15 @@ export class WeixinHarnessBridge {
|
|
|
515
603
|
this.#status.lastMessageError = null;
|
|
516
604
|
return delivery.receipt;
|
|
517
605
|
} catch (error) {
|
|
606
|
+
let batchFailureMessage = null;
|
|
607
|
+
if (!batchSettled && batchSubmission) {
|
|
608
|
+
if (error?.code === 'turn-stopped') {
|
|
609
|
+
this.#batchInputs.complete(key, batchSubmission.token);
|
|
610
|
+
} else {
|
|
611
|
+
batchFailureMessage = this.#batchInputs.fail(key, batchSubmission.token).message ?? null;
|
|
612
|
+
}
|
|
613
|
+
batchSettled = true;
|
|
614
|
+
}
|
|
518
615
|
if (error?.code === 'turn-stopped') {
|
|
519
616
|
await this.#state.markSeen(messageId);
|
|
520
617
|
return;
|
|
@@ -529,7 +626,7 @@ export class WeixinHarnessBridge {
|
|
|
529
626
|
try {
|
|
530
627
|
await this.#send(
|
|
531
628
|
sender,
|
|
532
|
-
userMessage,
|
|
629
|
+
batchFailureMessage ? `${userMessage}\n\n${batchFailureMessage}` : userMessage,
|
|
533
630
|
contextToken,
|
|
534
631
|
runId,
|
|
535
632
|
);
|
|
@@ -73,6 +73,23 @@ function delay(ms, signal) {
|
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
export function orderWeixinMessages(messages) {
|
|
77
|
+
if (!Array.isArray(messages) || messages.length < 2) return Array.isArray(messages) ? messages : [];
|
|
78
|
+
const orderField = ['seq', 'create_time_ms'].find((field) => messages.every((message) => (
|
|
79
|
+
(typeof message?.[field] === 'number' && Number.isFinite(message[field]))
|
|
80
|
+
|| (typeof message?.[field] === 'string'
|
|
81
|
+
&& message[field].trim()
|
|
82
|
+
&& Number.isFinite(Number(message[field])))
|
|
83
|
+
)));
|
|
84
|
+
if (!orderField) return messages;
|
|
85
|
+
return messages
|
|
86
|
+
.map((message, index) => ({ message, index, order: Number(message[orderField]) }))
|
|
87
|
+
.sort((left, right) => (
|
|
88
|
+
left.order - right.order || left.index - right.index
|
|
89
|
+
))
|
|
90
|
+
.map(({ message }) => message);
|
|
91
|
+
}
|
|
92
|
+
|
|
76
93
|
export function createWeixinRuntimeStatus() {
|
|
77
94
|
return {
|
|
78
95
|
startedAt: null,
|
|
@@ -234,7 +251,7 @@ export class WeixinRuntime {
|
|
|
234
251
|
this.#status.lastCheckedAt = Date.now();
|
|
235
252
|
this.#status.lastError = null;
|
|
236
253
|
|
|
237
|
-
for (const message of response?.msgs
|
|
254
|
+
for (const message of orderWeixinMessages(response?.msgs)) {
|
|
238
255
|
void this.#bridge.accept(message).catch((error) => {
|
|
239
256
|
if (signal.aborted) return;
|
|
240
257
|
this.#logger.error?.(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
areJidsSameUser,
|
|
@@ -217,9 +217,9 @@ export function normalizeWhatsappMessage(message, accountJid, {
|
|
|
217
217
|
const fromMe = message.key.fromMe === true;
|
|
218
218
|
const selfChat = fromMe && !group
|
|
219
219
|
&& [remoteJid, alternateRemoteJid].some((jid) => jid && areJidsSameUser(jid, accountJid));
|
|
220
|
-
if (fromMe && !selfChat) return null;
|
|
221
|
-
const senderJid =
|
|
222
|
-
const senderAlternateJid = group ? message.key.participantAlt : alternateRemoteJid;
|
|
220
|
+
if (fromMe && !selfChat && !group) return null;
|
|
221
|
+
const senderJid = fromMe ? accountJid : group ? message.key.participant : remoteJid;
|
|
222
|
+
const senderAlternateJid = group && !fromMe ? message.key.participantAlt : alternateRemoteJid;
|
|
223
223
|
if (typeof senderJid !== 'string' || !senderJid) return null;
|
|
224
224
|
const viewOnce = hasViewOnceWrapper(message.message);
|
|
225
225
|
const content = normalizeMessageContent(message.message);
|
|
@@ -239,9 +239,11 @@ export function normalizeWhatsappMessage(message, accountJid, {
|
|
|
239
239
|
kind: group ? 'group' : 'direct',
|
|
240
240
|
conversationId: remoteJid,
|
|
241
241
|
content: messageText(content),
|
|
242
|
+
plainText: typeof content?.conversation === 'string'
|
|
243
|
+
|| typeof content?.extendedTextMessage?.text === 'string',
|
|
242
244
|
images: image ? [image] : [],
|
|
243
245
|
files: file ? [file] : [],
|
|
244
|
-
addressed: !group || mentioned || replyToSelf,
|
|
246
|
+
addressed: !group || fromMe || mentioned || replyToSelf,
|
|
245
247
|
selfChat,
|
|
246
248
|
replyTarget: { jid: remoteJid, quoted: message, selfChat },
|
|
247
249
|
};
|
|
@@ -272,6 +274,14 @@ class RecentWhatsappOutboundIds {
|
|
|
272
274
|
}
|
|
273
275
|
|
|
274
276
|
remember(id) {
|
|
277
|
+
this.#store(id);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
reserve(id) {
|
|
281
|
+
this.#store(id);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#store(id) {
|
|
275
285
|
if (typeof id !== 'string' || !id) return;
|
|
276
286
|
this.#purge();
|
|
277
287
|
this.#ids.set(id, Date.now() + 5 * 60_000);
|
|
@@ -360,10 +370,23 @@ export class WhatsappBotClient {
|
|
|
360
370
|
await this.#stopTyping(target.jid);
|
|
361
371
|
const providerMessageIds = [];
|
|
362
372
|
for (const [index, chunk] of splitMessageText(text, 4_000).entries()) {
|
|
373
|
+
const messageId = randomBytes(10).toString('hex').toUpperCase();
|
|
374
|
+
const options = {
|
|
375
|
+
...(index === 0 && target.quoted ? { quoted: target.quoted } : {}),
|
|
376
|
+
messageId,
|
|
377
|
+
};
|
|
378
|
+
// Linked-account group messages are valid inbound prompts in open mode.
|
|
379
|
+
// Reserve our own id before dispatch so an early local echo cannot loop
|
|
380
|
+
// back through the bridge as another owner-authored group prompt.
|
|
381
|
+
if (typeof this.#outboundIds.reserve === 'function') {
|
|
382
|
+
this.#outboundIds.reserve(messageId);
|
|
383
|
+
} else {
|
|
384
|
+
this.#outboundIds.remember(messageId);
|
|
385
|
+
}
|
|
363
386
|
const result = await this.#socket.sendMessage(
|
|
364
387
|
target.jid,
|
|
365
388
|
{ text: chunk },
|
|
366
|
-
|
|
389
|
+
options,
|
|
367
390
|
);
|
|
368
391
|
this.#outboundIds.remember(result?.key?.id);
|
|
369
392
|
if (typeof result?.key?.id === 'string' && result.key.id) {
|