@xmanrui/dsh-im 4.5.0 → 4.7.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 +2 -0
- package/README.md +2 -0
- package/lib/client.js +4 -1
- package/lib/index.js +249 -240
- package/package.json +1 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +167 -20
- package/src/channels/dingtalk/dingtalk-card-stream.mjs +9 -2
- package/src/channels/dingtalk/state-store.mjs +98 -0
- package/src/channels/discord/discord-api.mjs +7 -0
- package/src/channels/discord/discord-runtime.mjs +97 -2
- package/src/channels/feishu/bridge.mjs +44 -13
- package/src/channels/feishu/feishu-cards.mjs +2 -0
- package/src/channels/feishu/message-utils.mjs +229 -0
- package/src/channels/qq/qq-bridge.mjs +49 -6
- package/src/channels/shared/batch-input.mjs +3 -3
- package/src/channels/shared/harness-client.mjs +82 -30
- package/src/channels/shared/i18n-en/feishu.mjs +2 -2
- package/src/channels/shared/i18n-en/shared-a.mjs +2 -0
- package/src/channels/shared/i18n-en/shared-b.mjs +2 -2
- package/src/channels/shared/i18n-en/shared-c.mjs +6 -4
- package/src/channels/shared/image-prompt.mjs +51 -0
- package/src/channels/shared/semantic/reply-reference.mjs +153 -0
- package/src/channels/shared/session-reply-recovery.mjs +104 -0
- package/src/channels/shared/session-title.mjs +1 -1
- package/src/channels/shared/text-harness-bridge.mjs +13 -6
- package/src/channels/shared/workspace-command.mjs +22 -3
- package/src/channels/slack/manifest.mjs +3 -0
- package/src/channels/slack/slack-api.mjs +18 -0
- package/src/channels/slack/slack-runtime.mjs +56 -0
- package/src/channels/telegram/telegram-runtime.mjs +117 -2
- package/src/channels/wecom/wecom-bridge.mjs +50 -7
- package/src/channels/weixin/state-store.mjs +110 -0
- package/src/channels/weixin/weixin-api.mjs +86 -2
- package/src/channels/weixin/weixin-bridge.mjs +97 -9
- package/src/channels/weixin/weixin-runtime.mjs +26 -6
- package/src/channels/whatsapp/whatsapp-runtime.mjs +56 -0
|
@@ -21,6 +21,9 @@ const ILINK_CLIENT_VERSION = (2 << 16) | (4 << 8) | 6;
|
|
|
21
21
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
22
22
|
const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
|
|
23
23
|
const WEIXIN_CDN_UPLOAD_RETRIES = 3;
|
|
24
|
+
const WEIXIN_MESSAGE_ID_TIMESTAMP_SHIFT = 22n;
|
|
25
|
+
const WEIXIN_MESSAGE_ID_MIN_TIMESTAMP_MS = Date.UTC(2020, 0, 1);
|
|
26
|
+
const WEIXIN_MESSAGE_ID_MAX_FUTURE_MS = 24 * 60 * 60 * 1_000;
|
|
24
27
|
const LOGIN_STATUSES = new Set([
|
|
25
28
|
'wait',
|
|
26
29
|
'scaned',
|
|
@@ -689,6 +692,7 @@ export function createWeixinApi({ fetchImpl = fetch } = {}) {
|
|
|
689
692
|
const recipient = nonEmptyString(toUserId);
|
|
690
693
|
const content = nonEmptyString(text);
|
|
691
694
|
if (!recipient || !content) throw new TypeError('toUserId and text are required');
|
|
695
|
+
const clientId = `dsh-weixin-${randomUUID()}`;
|
|
692
696
|
const response = await requestJson(fetchImpl, {
|
|
693
697
|
method: 'POST',
|
|
694
698
|
baseUrl,
|
|
@@ -699,7 +703,7 @@ export function createWeixinApi({ fetchImpl = fetch } = {}) {
|
|
|
699
703
|
msg: {
|
|
700
704
|
from_user_id: '',
|
|
701
705
|
to_user_id: recipient,
|
|
702
|
-
client_id:
|
|
706
|
+
client_id: clientId,
|
|
703
707
|
message_type: 2,
|
|
704
708
|
message_state: 2,
|
|
705
709
|
item_list: [{ type: 1, text_item: { text: content } }],
|
|
@@ -717,7 +721,10 @@ export function createWeixinApi({ fetchImpl = fetch } = {}) {
|
|
|
717
721
|
{ providerCode: sendRejection },
|
|
718
722
|
);
|
|
719
723
|
}
|
|
720
|
-
return
|
|
724
|
+
return {
|
|
725
|
+
...(response && typeof response === 'object' ? response : {}),
|
|
726
|
+
providerMessageIds: [clientId],
|
|
727
|
+
};
|
|
721
728
|
},
|
|
722
729
|
|
|
723
730
|
async sendFile(request) {
|
|
@@ -791,6 +798,83 @@ export function extractWeixinText(message) {
|
|
|
791
798
|
return null;
|
|
792
799
|
}
|
|
793
800
|
|
|
801
|
+
function weixinReplyAttachment(item) {
|
|
802
|
+
if (item?.type === 2 || (item?.image_item && typeof item.image_item === 'object')) {
|
|
803
|
+
return { kind: 'image' };
|
|
804
|
+
}
|
|
805
|
+
if (item?.type === 3 || (item?.voice_item && typeof item.voice_item === 'object')) {
|
|
806
|
+
return { kind: 'audio' };
|
|
807
|
+
}
|
|
808
|
+
if (item?.type === 4 || (item?.file_item && typeof item.file_item === 'object')) {
|
|
809
|
+
const name = nonEmptyString(item?.file_item?.file_name);
|
|
810
|
+
return { kind: 'file', ...(name ? { name } : {}) };
|
|
811
|
+
}
|
|
812
|
+
if (item?.type === 5 || (item?.video_item && typeof item.video_item === 'object')) {
|
|
813
|
+
return { kind: 'video' };
|
|
814
|
+
}
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function weixinQuotedText(item) {
|
|
819
|
+
const text = nonEmptyString(item?.text_item?.text);
|
|
820
|
+
if (text) return text;
|
|
821
|
+
return nonEmptyString(item?.voice_item?.text);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/** Extract the one-level reply snapshot embedded in an inbound iLink message item. */
|
|
825
|
+
export function extractWeixinReplyReference(message, { resolveContent, loadContent } = {}) {
|
|
826
|
+
const container = (message?.item_list ?? []).find((item) => (
|
|
827
|
+
item?.ref_msg && typeof item.ref_msg === 'object'
|
|
828
|
+
));
|
|
829
|
+
if (!container) return null;
|
|
830
|
+
const ref = container.ref_msg;
|
|
831
|
+
const quotedItem = ref.message_item && typeof ref.message_item === 'object'
|
|
832
|
+
? ref.message_item
|
|
833
|
+
: null;
|
|
834
|
+
const quotedText = weixinQuotedText(quotedItem);
|
|
835
|
+
let content = quotedText ?? nonEmptyString(ref.title);
|
|
836
|
+
const attachment = weixinReplyAttachment(quotedItem);
|
|
837
|
+
const messageId = quotedItem?.msg_id === undefined || quotedItem?.msg_id === null
|
|
838
|
+
? null
|
|
839
|
+
: nonEmptyString(String(quotedItem.msg_id));
|
|
840
|
+
const referenceDetails = {
|
|
841
|
+
messageId,
|
|
842
|
+
createTimeMs: quotedItem?.create_time_ms ?? ref.create_time_ms,
|
|
843
|
+
updateTimeMs: quotedItem?.update_time_ms ?? ref.update_time_ms,
|
|
844
|
+
};
|
|
845
|
+
if (!content && !attachment && typeof resolveContent === 'function') {
|
|
846
|
+
content = nonEmptyString(resolveContent(referenceDetails));
|
|
847
|
+
}
|
|
848
|
+
const load = !content && !attachment && typeof loadContent === 'function'
|
|
849
|
+
? (options) => loadContent(referenceDetails, options)
|
|
850
|
+
: null;
|
|
851
|
+
const knownType = quotedItem && [1, 2, 3, 4, 5, 8].includes(quotedItem.type);
|
|
852
|
+
return {
|
|
853
|
+
...(messageId ? { messageId } : {}),
|
|
854
|
+
...(content ? { content } : {}),
|
|
855
|
+
...(attachment ? { attachments: [attachment] } : {}),
|
|
856
|
+
...(load ? { load } : {}),
|
|
857
|
+
...(!content && !attachment && !load
|
|
858
|
+
? { unavailableReason: quotedItem && !knownType ? 'unsupported' : 'not-delivered' }
|
|
859
|
+
: {}),
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/** Decode the millisecond timestamp carried by current 64-bit iLink message IDs. */
|
|
864
|
+
export function weixinMessageTimestampMs(messageId, { now = Date.now() } = {}) {
|
|
865
|
+
const value = messageId === undefined || messageId === null ? '' : String(messageId).trim();
|
|
866
|
+
if (!/^\d{16,20}$/u.test(value)) return null;
|
|
867
|
+
try {
|
|
868
|
+
const timestampMs = Number(BigInt(value) >> WEIXIN_MESSAGE_ID_TIMESTAMP_SHIFT);
|
|
869
|
+
if (!Number.isSafeInteger(timestampMs)
|
|
870
|
+
|| timestampMs < WEIXIN_MESSAGE_ID_MIN_TIMESTAMP_MS
|
|
871
|
+
|| timestampMs > now + WEIXIN_MESSAGE_ID_MAX_FUTURE_MS) return null;
|
|
872
|
+
return timestampMs;
|
|
873
|
+
} catch {
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
794
878
|
export function weixinMessageId(message) {
|
|
795
879
|
if (message?.message_id !== undefined && message.message_id !== null) {
|
|
796
880
|
return String(message.message_id);
|
|
@@ -2,9 +2,11 @@ import {
|
|
|
2
2
|
DEFAULT_WEIXIN_MAX_MESSAGE_CHARS,
|
|
3
3
|
extractWeixinFiles,
|
|
4
4
|
extractWeixinImages,
|
|
5
|
+
extractWeixinReplyReference,
|
|
5
6
|
extractWeixinText,
|
|
6
7
|
splitWeixinText,
|
|
7
8
|
weixinMessageId,
|
|
9
|
+
weixinMessageTimestampMs,
|
|
8
10
|
} from './weixin-api.mjs';
|
|
9
11
|
import {
|
|
10
12
|
harnessAnswerForQuestion,
|
|
@@ -38,7 +40,6 @@ import {
|
|
|
38
40
|
hasInboundImages,
|
|
39
41
|
imagePromptDiagnostic,
|
|
40
42
|
imagePromptUserMessage,
|
|
41
|
-
promptContentForMessage,
|
|
42
43
|
} from '../shared/image-prompt.mjs';
|
|
43
44
|
import {
|
|
44
45
|
hasInboundFiles,
|
|
@@ -47,10 +48,15 @@ import {
|
|
|
47
48
|
} from '../shared/inbound-file.mjs';
|
|
48
49
|
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
49
50
|
import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
|
|
51
|
+
import {
|
|
52
|
+
hasReplyReference,
|
|
53
|
+
promptContentForInboundMessage,
|
|
54
|
+
} from '../shared/semantic/reply-reference.mjs';
|
|
50
55
|
import {
|
|
51
56
|
createDeliveryReceipt,
|
|
52
57
|
providerMessageIdsFor,
|
|
53
58
|
} from '../shared/semantic/delivery.mjs';
|
|
59
|
+
import { recoverAssistantTextByTimestamp } from '../shared/session-reply-recovery.mjs';
|
|
54
60
|
import {
|
|
55
61
|
channelDeliveryFailure,
|
|
56
62
|
clearLastMessageFailure,
|
|
@@ -67,6 +73,10 @@ const INTERACTION_RESOLVED_TEXT = () => t('这个问题已在其他客户端处
|
|
|
67
73
|
const DEFAULT_TYPING_KEEPALIVE_MS = 5_000;
|
|
68
74
|
const TYPING_RETRY_DELAY_MS = 60_000;
|
|
69
75
|
const WEIXIN_SEND_DIAGNOSTIC = Symbol('weixin-send-diagnostic');
|
|
76
|
+
const WEIXIN_REPLY_HISTORY_PAGE_SIZE = 100;
|
|
77
|
+
const WEIXIN_REPLY_HISTORY_MAX_PAGES = 3;
|
|
78
|
+
const WEIXIN_REPLY_HISTORY_TIMEOUT_MS = 5_000;
|
|
79
|
+
const WEIXIN_REPLY_HISTORY_MATCH_TOLERANCE_MS = 15_000;
|
|
70
80
|
|
|
71
81
|
const HELP_TEXT = () => [
|
|
72
82
|
t('微信已连接 DeepSeek Harness。'),
|
|
@@ -78,6 +88,7 @@ const HELP_TEXT = () => [
|
|
|
78
88
|
t('/workspace 工作区序号或绝对路径 切换工作区'),
|
|
79
89
|
t('/workspacelist 列出工作区绝对路径'),
|
|
80
90
|
t('/sessionlist 或 /sessions [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
|
91
|
+
t('/sessionlist --limit N 仅列出当前工作区前 N 个会话'),
|
|
81
92
|
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
|
82
93
|
t('/models 按序号列出所有可用模型'),
|
|
83
94
|
t('/reasoninglist 或 /reasonings 按序号列出当前模型可用推理等级'),
|
|
@@ -182,7 +193,20 @@ function weixinSendFailureOptions(error) {
|
|
|
182
193
|
return undefined;
|
|
183
194
|
}
|
|
184
195
|
|
|
185
|
-
export function weixinInboundMessage(message, api) {
|
|
196
|
+
export function weixinInboundMessage(message, api, state, { loadReplyContent } = {}) {
|
|
197
|
+
const toUserId = nonEmptyString(message?.from_user_id);
|
|
198
|
+
const replyTo = extractWeixinReplyReference(message, {
|
|
199
|
+
resolveContent: (reference) => state?.recentOutboundTextFor?.({
|
|
200
|
+
toUserId,
|
|
201
|
+
...reference,
|
|
202
|
+
}),
|
|
203
|
+
...(typeof loadReplyContent === 'function' ? {
|
|
204
|
+
loadContent: (reference, options) => loadReplyContent({
|
|
205
|
+
toUserId,
|
|
206
|
+
...reference,
|
|
207
|
+
}, options),
|
|
208
|
+
} : {}),
|
|
209
|
+
});
|
|
186
210
|
return {
|
|
187
211
|
content: extractWeixinText(message) ?? '',
|
|
188
212
|
images: typeof api?.inboundImages === 'function'
|
|
@@ -191,6 +215,7 @@ export function weixinInboundMessage(message, api) {
|
|
|
191
215
|
files: typeof api?.inboundFiles === 'function'
|
|
192
216
|
? api.inboundFiles(message)
|
|
193
217
|
: extractWeixinFiles(message),
|
|
218
|
+
...(replyTo ? { replyTo } : {}),
|
|
194
219
|
};
|
|
195
220
|
}
|
|
196
221
|
|
|
@@ -375,7 +400,8 @@ export class WeixinHarnessBridge {
|
|
|
375
400
|
&& (this.#queues.has(key) || pending || this.#approvals.hasPending(key))
|
|
376
401
|
? { handled: true, kind: 'busy', message: batchInputBusyMessage() }
|
|
377
402
|
: this.#batchInputs.handle(key, commandText, {
|
|
378
|
-
plainText: isNativeWeixinText(message)
|
|
403
|
+
plainText: isNativeWeixinText(message)
|
|
404
|
+
&& !extractWeixinReplyReference(message),
|
|
379
405
|
});
|
|
380
406
|
if (result.handled) {
|
|
381
407
|
if (result.kind === 'submit') {
|
|
@@ -489,7 +515,7 @@ export class WeixinHarnessBridge {
|
|
|
489
515
|
batchSubmission = null,
|
|
490
516
|
} = {}) {
|
|
491
517
|
const preparedMessage = prefetchInboundFiles(
|
|
492
|
-
|
|
518
|
+
this.#inboundMessage(message),
|
|
493
519
|
{ signal: this.#signal },
|
|
494
520
|
);
|
|
495
521
|
const previous = this.#queues.get(key) ?? Promise.resolve();
|
|
@@ -508,6 +534,54 @@ export class WeixinHarnessBridge {
|
|
|
508
534
|
return current;
|
|
509
535
|
}
|
|
510
536
|
|
|
537
|
+
#inboundMessage(message) {
|
|
538
|
+
return weixinInboundMessage(message, this.#api, this.#state, {
|
|
539
|
+
loadReplyContent: (reference, options) => this.#loadReplyContent(reference, options),
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async #loadReplyContent(reference, { signal: callerSignal } = {}) {
|
|
544
|
+
const indexed = this.#state.recentOutboundTextFor?.(reference);
|
|
545
|
+
if (indexed) return { content: indexed };
|
|
546
|
+
const quotedAt = [
|
|
547
|
+
weixinMessageTimestampMs(reference?.messageId),
|
|
548
|
+
Number(reference?.createTimeMs),
|
|
549
|
+
Number(reference?.updateTimeMs),
|
|
550
|
+
].find(Number.isSafeInteger);
|
|
551
|
+
if (quotedAt === undefined) return { unavailableReason: 'not-delivered' };
|
|
552
|
+
const sender = nonEmptyString(reference?.toUserId);
|
|
553
|
+
const sessionId = sender ? this.#state.sessionFor(conversationKey(sender)) : null;
|
|
554
|
+
const session = typeof sessionId === 'string' && sessionId
|
|
555
|
+
? this.#harness.workspaceSession?.(sessionId)
|
|
556
|
+
: null;
|
|
557
|
+
if (typeof session?.readHistory !== 'function') {
|
|
558
|
+
return { unavailableReason: 'not-delivered' };
|
|
559
|
+
}
|
|
560
|
+
const text = await recoverAssistantTextByTimestamp({
|
|
561
|
+
session,
|
|
562
|
+
quotedAt,
|
|
563
|
+
signal: callerSignal,
|
|
564
|
+
pageSize: WEIXIN_REPLY_HISTORY_PAGE_SIZE,
|
|
565
|
+
maxPages: WEIXIN_REPLY_HISTORY_MAX_PAGES,
|
|
566
|
+
timeoutMs: WEIXIN_REPLY_HISTORY_TIMEOUT_MS,
|
|
567
|
+
toleranceMs: WEIXIN_REPLY_HISTORY_MATCH_TOLERANCE_MS,
|
|
568
|
+
});
|
|
569
|
+
if (!text) return { unavailableReason: 'not-delivered' };
|
|
570
|
+
const messageId = nonEmptyString(reference?.messageId);
|
|
571
|
+
try {
|
|
572
|
+
await this.#state.rememberOutboundMessage?.({
|
|
573
|
+
toUserId: sender,
|
|
574
|
+
text,
|
|
575
|
+
sentAt: quotedAt,
|
|
576
|
+
completedAt: quotedAt,
|
|
577
|
+
providerMessageIds: messageId ? [messageId] : [],
|
|
578
|
+
});
|
|
579
|
+
} catch (error) {
|
|
580
|
+
this.#logger.warn?.('[dsh-weixin] failed to remember a recovered quote:', error);
|
|
581
|
+
}
|
|
582
|
+
return { content: text };
|
|
583
|
+
}
|
|
584
|
+
|
|
511
585
|
#finishBatchResult(message, messageId, key, sender, contextToken, runId, result) {
|
|
512
586
|
let task;
|
|
513
587
|
task = Promise.resolve().then(async () => {
|
|
@@ -644,11 +718,12 @@ export class WeixinHarnessBridge {
|
|
|
644
718
|
let batchSettled = batchSubmission === null;
|
|
645
719
|
let promptRecorded = false;
|
|
646
720
|
try {
|
|
647
|
-
const promptMessage = preparedMessage ??
|
|
721
|
+
const promptMessage = preparedMessage ?? this.#inboundMessage(message);
|
|
648
722
|
const text = promptMessage.content;
|
|
649
723
|
const hasImages = hasInboundImages(promptMessage);
|
|
650
724
|
const hasFiles = hasInboundFiles(promptMessage);
|
|
651
|
-
|
|
725
|
+
const hasReply = hasReplyReference(promptMessage);
|
|
726
|
+
if (!text && !hasImages && !hasFiles && !hasReply) {
|
|
652
727
|
await this.#send(sender, t('目前支持文字、图片、文件,以及微信已转成文字的语音消息。'), contextToken, runId);
|
|
653
728
|
await this.#state.markSeen(messageId);
|
|
654
729
|
return;
|
|
@@ -701,8 +776,8 @@ export class WeixinHarnessBridge {
|
|
|
701
776
|
let artifacts = [];
|
|
702
777
|
await this.#startTyping(sender, contextToken);
|
|
703
778
|
try {
|
|
704
|
-
let content = hasImages
|
|
705
|
-
? await
|
|
779
|
+
let content = hasImages || hasReply
|
|
780
|
+
? await promptContentForInboundMessage(promptMessage, { signal: this.#signal })
|
|
706
781
|
: undefined;
|
|
707
782
|
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
708
783
|
let contextEnhanced = false;
|
|
@@ -1154,6 +1229,7 @@ export class WeixinHarnessBridge {
|
|
|
1154
1229
|
const chunks = splitWeixinText(text, this.#maxMessageChars);
|
|
1155
1230
|
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) {
|
|
1156
1231
|
const chunk = chunks[chunkIndex];
|
|
1232
|
+
const sentAt = Date.now();
|
|
1157
1233
|
try {
|
|
1158
1234
|
const result = await this.#api.sendText({
|
|
1159
1235
|
baseUrl: this.#baseUrl,
|
|
@@ -1164,7 +1240,19 @@ export class WeixinHarnessBridge {
|
|
|
1164
1240
|
runId,
|
|
1165
1241
|
signal: this.#signal,
|
|
1166
1242
|
});
|
|
1167
|
-
|
|
1243
|
+
const chunkMessageIds = providerMessageIdsFor(result);
|
|
1244
|
+
providerMessageIds.push(...chunkMessageIds);
|
|
1245
|
+
try {
|
|
1246
|
+
await this.#state.rememberOutboundMessage?.({
|
|
1247
|
+
toUserId,
|
|
1248
|
+
text: chunk,
|
|
1249
|
+
sentAt,
|
|
1250
|
+
completedAt: Date.now(),
|
|
1251
|
+
providerMessageIds: chunkMessageIds,
|
|
1252
|
+
});
|
|
1253
|
+
} catch (error) {
|
|
1254
|
+
this.#logger.warn?.('[dsh-weixin] failed to remember an outbound message:', error);
|
|
1255
|
+
}
|
|
1168
1256
|
} catch (error) {
|
|
1169
1257
|
throw weixinSendError(error, {
|
|
1170
1258
|
baseUrl: this.#baseUrl,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_WEIXIN_MAX_MESSAGE_CHARS, WeixinApiError } from './weixin-api.mjs';
|
|
2
2
|
import { createWeixinBridgeStatus, WeixinHarnessBridge } from './weixin-bridge.mjs';
|
|
3
|
+
import { providerMessageIdsFor } from '../shared/semantic/delivery.mjs';
|
|
3
4
|
import {
|
|
4
5
|
connectionTestTarget,
|
|
5
6
|
connectionTestTargetUnavailable,
|
|
@@ -324,9 +325,7 @@ export class WeixinRuntime {
|
|
|
324
325
|
if (!this.#status.ready || !this.#abortController) {
|
|
325
326
|
throw new Error('Weixin runtime is not connected');
|
|
326
327
|
}
|
|
327
|
-
await this.#
|
|
328
|
-
baseUrl: this.#config.baseUrl,
|
|
329
|
-
token: this.#token,
|
|
328
|
+
await this.#sendTrackedText({
|
|
330
329
|
toUserId,
|
|
331
330
|
text,
|
|
332
331
|
signal: this.#abortController.signal,
|
|
@@ -334,6 +333,29 @@ export class WeixinRuntime {
|
|
|
334
333
|
return { sent: true };
|
|
335
334
|
}
|
|
336
335
|
|
|
336
|
+
async #sendTrackedText({ toUserId, text, signal }) {
|
|
337
|
+
const sentAt = Date.now();
|
|
338
|
+
const result = await this.#api.sendText({
|
|
339
|
+
baseUrl: this.#config.baseUrl,
|
|
340
|
+
token: this.#token,
|
|
341
|
+
toUserId,
|
|
342
|
+
text,
|
|
343
|
+
signal,
|
|
344
|
+
});
|
|
345
|
+
try {
|
|
346
|
+
await this.#state.rememberOutboundMessage?.({
|
|
347
|
+
toUserId,
|
|
348
|
+
text,
|
|
349
|
+
sentAt,
|
|
350
|
+
completedAt: Date.now(),
|
|
351
|
+
providerMessageIds: providerMessageIdsFor(result),
|
|
352
|
+
});
|
|
353
|
+
} catch (error) {
|
|
354
|
+
this.#logger.warn?.('[dsh-weixin] failed to remember an outbound message:', error);
|
|
355
|
+
}
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
|
|
337
359
|
async sendProactiveText(target, text, { signal } = {}) {
|
|
338
360
|
const toUserId = typeof target?.route?.toUserId === 'string'
|
|
339
361
|
? target.route.toUserId.trim() : '';
|
|
@@ -348,9 +370,7 @@ export class WeixinRuntime {
|
|
|
348
370
|
throw error;
|
|
349
371
|
}
|
|
350
372
|
signal?.throwIfAborted();
|
|
351
|
-
await this.#
|
|
352
|
-
baseUrl: this.#config.baseUrl,
|
|
353
|
-
token: this.#token,
|
|
373
|
+
await this.#sendTrackedText({
|
|
354
374
|
toUserId,
|
|
355
375
|
text,
|
|
356
376
|
signal: signal ?? this.#abortController.signal,
|
|
@@ -96,6 +96,60 @@ function messageText(content) {
|
|
|
96
96
|
?? '';
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
function whatsappReplyAttachment(kind, media, fallbackName) {
|
|
100
|
+
if (!media || typeof media !== 'object') return null;
|
|
101
|
+
const name = typeof media.fileName === 'string' && media.fileName
|
|
102
|
+
? media.fileName : typeof fallbackName === 'string' && fallbackName
|
|
103
|
+
? fallbackName : undefined;
|
|
104
|
+
return { kind, ...(name ? { name } : {}) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function whatsappReplyAttachments(content) {
|
|
108
|
+
const attachments = [];
|
|
109
|
+
if (content?.imageMessage) {
|
|
110
|
+
attachments.push(whatsappReplyAttachment('image', content.imageMessage));
|
|
111
|
+
}
|
|
112
|
+
if (content?.documentMessage) {
|
|
113
|
+
const mediaType = typeof content.documentMessage.mimetype === 'string'
|
|
114
|
+
? content.documentMessage.mimetype.toLowerCase() : '';
|
|
115
|
+
attachments.push(whatsappReplyAttachment(
|
|
116
|
+
mediaType.startsWith('image/') ? 'image' : 'file',
|
|
117
|
+
content.documentMessage,
|
|
118
|
+
));
|
|
119
|
+
}
|
|
120
|
+
if (content?.audioMessage) {
|
|
121
|
+
attachments.push(whatsappReplyAttachment('audio', content.audioMessage));
|
|
122
|
+
}
|
|
123
|
+
if (content?.videoMessage) {
|
|
124
|
+
attachments.push(whatsappReplyAttachment('video', content.videoMessage));
|
|
125
|
+
}
|
|
126
|
+
if (content?.stickerMessage) {
|
|
127
|
+
const mediaType = typeof content.stickerMessage.mimetype === 'string'
|
|
128
|
+
? content.stickerMessage.mimetype.toLowerCase() : '';
|
|
129
|
+
attachments.push(whatsappReplyAttachment(
|
|
130
|
+
mediaType.startsWith('video/') || content.stickerMessage.isAnimated === true
|
|
131
|
+
? 'video' : 'image',
|
|
132
|
+
content.stickerMessage,
|
|
133
|
+
));
|
|
134
|
+
}
|
|
135
|
+
return attachments.filter(Boolean);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function whatsappReplyReference(context) {
|
|
139
|
+
if (!context?.quotedMessage || typeof context.quotedMessage !== 'object') return undefined;
|
|
140
|
+
const content = normalizeMessageContent(context.quotedMessage);
|
|
141
|
+
const messageId = typeof context.stanzaId === 'string' && context.stanzaId
|
|
142
|
+
? context.stanzaId : undefined;
|
|
143
|
+
const authorId = typeof context.participant === 'string' && context.participant
|
|
144
|
+
? context.participant : undefined;
|
|
145
|
+
return {
|
|
146
|
+
...(messageId ? { messageId } : {}),
|
|
147
|
+
...(authorId ? { authorId } : {}),
|
|
148
|
+
content: messageText(content),
|
|
149
|
+
attachments: whatsappReplyAttachments(content),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
99
153
|
function mediaSize(value) {
|
|
100
154
|
if (Number.isSafeInteger(value) && value >= 0) return value;
|
|
101
155
|
let converted;
|
|
@@ -260,6 +314,7 @@ export function normalizeWhatsappMessage(message, accountJid, {
|
|
|
260
314
|
&& areJidsSameUser(context.participant, accountJid);
|
|
261
315
|
const image = whatsappImageSource(message, content, download, { viewOnce });
|
|
262
316
|
const file = whatsappFileSource(message, content, download);
|
|
317
|
+
const replyTo = whatsappReplyReference(context);
|
|
263
318
|
return {
|
|
264
319
|
messageId: `${remoteJid}:${messageId}`,
|
|
265
320
|
providerMessageId: messageId,
|
|
@@ -274,6 +329,7 @@ export function normalizeWhatsappMessage(message, accountJid, {
|
|
|
274
329
|
|| typeof content?.extendedTextMessage?.text === 'string',
|
|
275
330
|
images: image ? [image] : [],
|
|
276
331
|
files: file ? [file] : [],
|
|
332
|
+
...(replyTo ? { replyTo } : {}),
|
|
277
333
|
addressed: !group || fromMe || mentioned || replyToSelf,
|
|
278
334
|
selfChat,
|
|
279
335
|
replyTarget: { jid: remoteJid, quoted: message, selfChat },
|