@xmanrui/dsh-im 2.5.0 → 3.0.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 +4 -4
- package/README.md +4 -4
- package/bin/dsh-im.mjs +1 -1
- package/lib/client.js +1410 -1409
- package/lib/index.js +206 -206
- package/package.json +2 -2
- package/plugin-src/client/channels/dingtalk/index.js +0 -17
- package/plugin-src/client/channels/dingtalk/styles.js +2 -2
- package/plugin-src/client/channels/feishu/index.js +0 -29
- package/plugin-src/client/channels/feishu/styles.js +2 -2
- package/plugin-src/client/channels/qq/index.js +0 -9
- package/plugin-src/client/channels/wecom/index.js +0 -9
- package/plugin-src/client/channels/weixin/index.js +0 -19
- package/plugin-src/client/channels/weixin/styles.js +2 -2
- package/plugin-src/client/index.js +4 -4
- package/scripts/verify-package.mjs +48 -11
- package/src/channels/dingtalk/dingtalk-api.mjs +101 -5
- package/src/channels/dingtalk/dingtalk-bridge.mjs +198 -22
- package/src/channels/discord/discord-api.mjs +35 -0
- package/src/channels/discord/discord-bridge.mjs +1 -0
- package/src/channels/discord/discord-runtime.mjs +26 -0
- package/src/channels/feishu/bridge.mjs +21 -28
- package/src/channels/feishu/message-utils.mjs +1 -1
- package/src/channels/shared/connection-test.mjs +1 -1
- package/src/channels/shared/i18n-en/feishu.mjs +6 -6
- package/src/channels/shared/i18n-en/shared-c.mjs +2 -2
- package/src/channels/shared/status-reaction.mjs +107 -0
- package/src/channels/shared/text-harness-bridge.mjs +58 -6
- package/src/channels/slack/manifest.mjs +1 -0
- package/src/channels/slack/slack-api.mjs +28 -0
- package/src/channels/slack/slack-bridge.mjs +5 -0
- package/src/channels/slack/slack-runtime.mjs +24 -0
- package/src/channels/telegram/telegram-api.mjs +9 -0
- package/src/channels/telegram/telegram-bridge.mjs +1 -0
- package/src/channels/telegram/telegram-runtime.mjs +20 -0
- package/src/channels/whatsapp/whatsapp-bridge.mjs +1 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +36 -0
|
@@ -73,8 +73,8 @@ export default {
|
|
|
73
73
|
// connection-test.mjs
|
|
74
74
|
'✅ DeepSeek Harness 连接测试成功':
|
|
75
75
|
'✅ DeepSeek Harness connection test succeeded',
|
|
76
|
-
'
|
|
77
|
-
'This message was sent from the "{name}" bot card on the
|
|
76
|
+
'这条消息由「IM机器人」设置页中的“{name}”机器人卡片发出。':
|
|
77
|
+
'This message was sent from the "{name}" bot card on the IM Bot settings page.',
|
|
78
78
|
'{channelLabel}尚未收到可用于测试的私聊消息。':
|
|
79
79
|
'The {channelLabel} has not received a direct message that can be used for testing yet.',
|
|
80
80
|
'机器人': 'bot',
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const DEFAULT_TIMEOUT_MS = 2_000;
|
|
2
|
+
|
|
3
|
+
const NOOP_REACTION = Object.freeze({
|
|
4
|
+
success() {},
|
|
5
|
+
error() {},
|
|
6
|
+
clear() {},
|
|
7
|
+
settled: () => Promise.resolve(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
function increment(status, key) {
|
|
11
|
+
if (!status || typeof status !== 'object') return;
|
|
12
|
+
status[key] = (status[key] ?? 0) + 1;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function runWithTimeout(operation, timeoutMs) {
|
|
16
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
17
|
+
let onAbort;
|
|
18
|
+
const aborted = new Promise((_, reject) => {
|
|
19
|
+
onAbort = () => reject(signal.reason ?? new DOMException('Timed out', 'TimeoutError'));
|
|
20
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
return await Promise.race([operation(signal), aborted]);
|
|
24
|
+
} finally {
|
|
25
|
+
signal.removeEventListener('abort', onAbort);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Starts a best-effort status reaction lifecycle which is deliberately not
|
|
31
|
+
* part of the caller's message queue. Calls are serialized only for this one
|
|
32
|
+
* source message; every provider operation is bounded and absorbs failures.
|
|
33
|
+
*/
|
|
34
|
+
export function beginStatusReaction({
|
|
35
|
+
adapter,
|
|
36
|
+
target,
|
|
37
|
+
reactions,
|
|
38
|
+
status,
|
|
39
|
+
logger = console,
|
|
40
|
+
label = 'channel',
|
|
41
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
42
|
+
} = {}) {
|
|
43
|
+
if (!target
|
|
44
|
+
|| typeof adapter?.addReaction !== 'function'
|
|
45
|
+
|| typeof adapter?.removeReaction !== 'function'
|
|
46
|
+
|| typeof reactions?.processing !== 'string'
|
|
47
|
+
|| !reactions.processing
|
|
48
|
+
|| !Number.isSafeInteger(timeoutMs)
|
|
49
|
+
|| timeoutMs <= 0) return NOOP_REACTION;
|
|
50
|
+
|
|
51
|
+
let currentReaction = null;
|
|
52
|
+
let terminal = false;
|
|
53
|
+
|
|
54
|
+
const safely = async (kind, operation) => {
|
|
55
|
+
try {
|
|
56
|
+
const value = await runWithTimeout(operation, timeoutMs);
|
|
57
|
+
increment(status, kind === 'add' ? 'reactionsAdded' : 'reactionsRemoved');
|
|
58
|
+
return { ok: true, value };
|
|
59
|
+
} catch (cause) {
|
|
60
|
+
increment(status, 'reactionErrors');
|
|
61
|
+
logger.warn?.(
|
|
62
|
+
`[dsh-im:${label}] status reaction ${kind} failed:`,
|
|
63
|
+
cause?.message ?? cause?.name ?? String(cause),
|
|
64
|
+
);
|
|
65
|
+
return { ok: false, value: null };
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const transition = async (emoji) => {
|
|
70
|
+
if (currentReaction !== null) {
|
|
71
|
+
const previous = currentReaction;
|
|
72
|
+
currentReaction = null;
|
|
73
|
+
await safely('remove', (signal) => adapter.removeReaction(
|
|
74
|
+
target,
|
|
75
|
+
previous,
|
|
76
|
+
{ signal },
|
|
77
|
+
));
|
|
78
|
+
}
|
|
79
|
+
if (typeof emoji !== 'string' || !emoji) return;
|
|
80
|
+
const added = await safely('add', (signal) => adapter.addReaction(
|
|
81
|
+
target,
|
|
82
|
+
emoji,
|
|
83
|
+
{ signal },
|
|
84
|
+
));
|
|
85
|
+
if (added.ok && added.value !== undefined && added.value !== null) {
|
|
86
|
+
currentReaction = added.value;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Calling an async function starts the provider request synchronously up to
|
|
91
|
+
// its first await, while the returned tail remains completely detached from
|
|
92
|
+
// normal message processing.
|
|
93
|
+
let tail = transition(reactions.processing);
|
|
94
|
+
const finish = (emoji) => {
|
|
95
|
+
if (terminal) return;
|
|
96
|
+
terminal = true;
|
|
97
|
+
tail = tail.then(() => transition(emoji), () => transition(emoji));
|
|
98
|
+
void tail.catch(() => undefined);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return Object.freeze({
|
|
102
|
+
success: () => finish(reactions.success),
|
|
103
|
+
error: () => finish(reactions.error),
|
|
104
|
+
clear: () => finish(null),
|
|
105
|
+
settled: () => tail,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
messageFailureText,
|
|
53
53
|
setLastMessageFailure,
|
|
54
54
|
} from './message-failure.mjs';
|
|
55
|
+
import { beginStatusReaction } from './status-reaction.mjs';
|
|
55
56
|
|
|
56
57
|
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
57
58
|
const FILE_ONLY_COMPLETION_TEXT = '任务已完成。';
|
|
@@ -114,6 +115,9 @@ export function createTextBridgeStatus() {
|
|
|
114
115
|
lastRejectedAt: null,
|
|
115
116
|
lastError: null,
|
|
116
117
|
lastMessageError: null,
|
|
118
|
+
reactionsAdded: 0,
|
|
119
|
+
reactionsRemoved: 0,
|
|
120
|
+
reactionErrors: 0,
|
|
117
121
|
};
|
|
118
122
|
}
|
|
119
123
|
|
|
@@ -178,6 +182,27 @@ export class TextHarnessBridge {
|
|
|
178
182
|
return Promise.resolve();
|
|
179
183
|
}
|
|
180
184
|
this.#acceptedMessageIds.add(messageId);
|
|
185
|
+
const statusReaction = beginStatusReaction({
|
|
186
|
+
adapter: this.#bot,
|
|
187
|
+
target: normalized.kind === 'direct' || normalized.addressed === true
|
|
188
|
+
? normalized.reactionTarget
|
|
189
|
+
: null,
|
|
190
|
+
reactions: this.#descriptor.reactions,
|
|
191
|
+
status: this.#status,
|
|
192
|
+
logger: this.#logger,
|
|
193
|
+
label: this.#descriptor.key,
|
|
194
|
+
});
|
|
195
|
+
normalized.statusReaction = statusReaction;
|
|
196
|
+
|
|
197
|
+
const processing = this.#acceptAcceptedMessage(normalized, messageId, senderId);
|
|
198
|
+
void processing.then(
|
|
199
|
+
() => statusReaction.success(),
|
|
200
|
+
() => statusReaction.error(),
|
|
201
|
+
);
|
|
202
|
+
return processing;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
#acceptAcceptedMessage(normalized, messageId, senderId) {
|
|
181
206
|
if (normalized.kind === 'direct') {
|
|
182
207
|
rememberConnectionTestTarget(
|
|
183
208
|
this.#state,
|
|
@@ -185,7 +210,7 @@ export class TextHarnessBridge {
|
|
|
185
210
|
);
|
|
186
211
|
}
|
|
187
212
|
|
|
188
|
-
const key = `${kind}:${conversationId}`;
|
|
213
|
+
const key = `${normalized.kind}:${normalized.conversationId}`;
|
|
189
214
|
const pending = this.#pendingInteractions.get(key);
|
|
190
215
|
const text = cleanText(normalized.content);
|
|
191
216
|
const batchCommand = isBatchInputCommand(text);
|
|
@@ -326,7 +351,11 @@ export class TextHarnessBridge {
|
|
|
326
351
|
if (reply) await this.#bot.sendText(message.replyTarget, reply);
|
|
327
352
|
this.#status.lastError = null;
|
|
328
353
|
})().catch(async (error) => {
|
|
329
|
-
if (this.#signal?.aborted)
|
|
354
|
+
if (this.#signal?.aborted) {
|
|
355
|
+
message.statusReaction?.clear();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
message.statusReaction?.error();
|
|
330
359
|
this.#status.lastError = error?.message ?? String(error);
|
|
331
360
|
const failure = setLastMessageFailure(this.#status, error);
|
|
332
361
|
this.#logger.error?.(
|
|
@@ -406,7 +435,11 @@ export class TextHarnessBridge {
|
|
|
406
435
|
}
|
|
407
436
|
this.#status.lastError = null;
|
|
408
437
|
} catch (error) {
|
|
409
|
-
if (error?.code === 'turn-stopped' || this.#signal?.aborted)
|
|
438
|
+
if (error?.code === 'turn-stopped' || this.#signal?.aborted) {
|
|
439
|
+
message.statusReaction?.clear();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
message.statusReaction?.error();
|
|
410
443
|
this.#status.lastError = error?.message ?? String(error);
|
|
411
444
|
const failure = setLastMessageFailure(this.#status, error);
|
|
412
445
|
this.#logger.error?.(
|
|
@@ -706,6 +739,7 @@ export class TextHarnessBridge {
|
|
|
706
739
|
? this.#batches.fail(conversationKey, batchSubmission.token)
|
|
707
740
|
: null;
|
|
708
741
|
if (turnStopped) {
|
|
742
|
+
message.statusReaction?.clear();
|
|
709
743
|
if (stream) {
|
|
710
744
|
try {
|
|
711
745
|
await stream.finish(t('已停止。'));
|
|
@@ -716,9 +750,11 @@ export class TextHarnessBridge {
|
|
|
716
750
|
return;
|
|
717
751
|
}
|
|
718
752
|
if (this.#signal?.aborted) {
|
|
753
|
+
message.statusReaction?.clear();
|
|
719
754
|
stream?.cancel?.();
|
|
720
755
|
return;
|
|
721
756
|
}
|
|
757
|
+
message.statusReaction?.error();
|
|
722
758
|
this.#status.lastError = error?.message ?? String(error);
|
|
723
759
|
const presentStreamFailure = async (text) => {
|
|
724
760
|
const method = typeof stream?.fail === 'function'
|
|
@@ -773,7 +809,10 @@ export class TextHarnessBridge {
|
|
|
773
809
|
}
|
|
774
810
|
|
|
775
811
|
async #processInteractionReply(message, messageId, senderId, key, expected) {
|
|
776
|
-
if (this.#signal?.aborted)
|
|
812
|
+
if (this.#signal?.aborted) {
|
|
813
|
+
message.statusReaction?.clear();
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
777
816
|
const current = this.#pendingInteractions.get(key);
|
|
778
817
|
const claimed = expected.claimedReplyMessageId === messageId;
|
|
779
818
|
if (!current || current !== expected || current.submitting) {
|
|
@@ -827,6 +866,7 @@ export class TextHarnessBridge {
|
|
|
827
866
|
try {
|
|
828
867
|
await this.#presentInteraction(pending);
|
|
829
868
|
} catch (error) {
|
|
869
|
+
message.statusReaction?.error();
|
|
830
870
|
this.#status.lastError = t('{label}交互问题发送失败。', { label: this.#descriptor.label });
|
|
831
871
|
this.#logger.error?.(
|
|
832
872
|
`[dsh-im:${this.#descriptor.key}] failed to retry an interaction question:`,
|
|
@@ -865,6 +905,7 @@ export class TextHarnessBridge {
|
|
|
865
905
|
try {
|
|
866
906
|
await this.#presentInteraction(pending);
|
|
867
907
|
} catch (error) {
|
|
908
|
+
message.statusReaction?.error();
|
|
868
909
|
this.#status.lastError = t('{label}交互问题发送失败。', { label: this.#descriptor.label });
|
|
869
910
|
this.#logger.error?.(
|
|
870
911
|
`[dsh-im:${this.#descriptor.key}] failed to send the next interaction question:`,
|
|
@@ -889,7 +930,10 @@ export class TextHarnessBridge {
|
|
|
889
930
|
} catch (error) {
|
|
890
931
|
if (error?.code === 'interaction-not-pending') {
|
|
891
932
|
this.#clearPendingInteraction(key, pending.interactionId);
|
|
892
|
-
if (this.#signal?.aborted)
|
|
933
|
+
if (this.#signal?.aborted) {
|
|
934
|
+
message.statusReaction?.clear();
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
893
937
|
try {
|
|
894
938
|
await this.#bot.sendText(target, t(INTERACTION_RESOLVED_TEXT));
|
|
895
939
|
} catch (sendError) {
|
|
@@ -900,7 +944,15 @@ export class TextHarnessBridge {
|
|
|
900
944
|
}
|
|
901
945
|
return;
|
|
902
946
|
}
|
|
903
|
-
if (this.#signal?.aborted
|
|
947
|
+
if (this.#signal?.aborted) {
|
|
948
|
+
message.statusReaction?.clear();
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
if (this.#pendingInteractions.get(key) !== pending) {
|
|
952
|
+
message.statusReaction?.clear();
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
message.statusReaction?.error();
|
|
904
956
|
pending.submitting = false;
|
|
905
957
|
pending.answers.pop();
|
|
906
958
|
pending.index -= 1;
|
|
@@ -276,6 +276,34 @@ export class SlackApi {
|
|
|
276
276
|
});
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
addReaction({ channelId, messageTs, emojiName, signal, timeoutMs }) {
|
|
280
|
+
return this.#request('reactions.add', {
|
|
281
|
+
tokenKind: 'bot',
|
|
282
|
+
signal,
|
|
283
|
+
timeoutMs,
|
|
284
|
+
retry: false,
|
|
285
|
+
body: {
|
|
286
|
+
channel: slackId(channelId, 'channel id'),
|
|
287
|
+
timestamp: requiredString(messageTs, 'message timestamp'),
|
|
288
|
+
name: requiredString(emojiName, 'reaction name'),
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
removeReaction({ channelId, messageTs, emojiName, signal, timeoutMs }) {
|
|
294
|
+
return this.#request('reactions.remove', {
|
|
295
|
+
tokenKind: 'bot',
|
|
296
|
+
signal,
|
|
297
|
+
timeoutMs,
|
|
298
|
+
retry: false,
|
|
299
|
+
body: {
|
|
300
|
+
channel: slackId(channelId, 'channel id'),
|
|
301
|
+
timestamp: requiredString(messageTs, 'message timestamp'),
|
|
302
|
+
name: requiredString(emojiName, 'reaction name'),
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
279
307
|
startStream({ channelId, threadTs, recipientTeamId, recipientUserId, markdownText, signal }) {
|
|
280
308
|
return this.#request('chat.startStream', {
|
|
281
309
|
tokenKind: 'bot',
|
|
@@ -4,6 +4,11 @@ export const SLACK_DESCRIPTOR = Object.freeze({
|
|
|
4
4
|
key: 'slack',
|
|
5
5
|
label: 'Slack',
|
|
6
6
|
connectionLabel: ' Socket Mode 长连接',
|
|
7
|
+
reactions: Object.freeze({
|
|
8
|
+
processing: 'eyes',
|
|
9
|
+
success: 'white_check_mark',
|
|
10
|
+
error: 'x',
|
|
11
|
+
}),
|
|
7
12
|
});
|
|
8
13
|
|
|
9
14
|
export class SlackHarnessBridge extends TextHarnessBridge {
|
|
@@ -127,6 +127,10 @@ export function normalizeSlackEvent(payload, botUserId, {
|
|
|
127
127
|
? event.files.map((file) => slackFileSource(file, loadFileStream, loadFileInfo)).filter(Boolean)
|
|
128
128
|
: [],
|
|
129
129
|
addressed: direct || mentioned,
|
|
130
|
+
reactionTarget: {
|
|
131
|
+
channelId: String(event.channel),
|
|
132
|
+
messageTs: String(event.ts),
|
|
133
|
+
},
|
|
130
134
|
replyTarget: {
|
|
131
135
|
channelId: String(event.channel),
|
|
132
136
|
threadTs,
|
|
@@ -284,6 +288,26 @@ export class SlackBotClient {
|
|
|
284
288
|
return { providerMessageIds };
|
|
285
289
|
}
|
|
286
290
|
|
|
291
|
+
async addReaction(target, emoji, { signal } = {}) {
|
|
292
|
+
const reactionKey = String(emoji ?? '').trim();
|
|
293
|
+
await this.#api.addReaction({
|
|
294
|
+
channelId: target.channelId,
|
|
295
|
+
messageTs: target.messageTs,
|
|
296
|
+
emojiName: reactionKey,
|
|
297
|
+
signal: signal ?? this.#signal,
|
|
298
|
+
});
|
|
299
|
+
return reactionKey;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
removeReaction(target, reactionKey, { signal } = {}) {
|
|
303
|
+
return this.#api.removeReaction({
|
|
304
|
+
channelId: target.channelId,
|
|
305
|
+
messageTs: target.messageTs,
|
|
306
|
+
emojiName: reactionKey,
|
|
307
|
+
signal: signal ?? this.#signal,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
287
311
|
openStream(target) {
|
|
288
312
|
return createSlackMessageStream({
|
|
289
313
|
api: this.#api,
|
|
@@ -203,6 +203,15 @@ export class TelegramApi {
|
|
|
203
203
|
}, { signal });
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
async setMessageReaction({ chatId, messageId, emoji, signal, timeoutMs }) {
|
|
207
|
+
const normalizedEmoji = cleanString(emoji);
|
|
208
|
+
return this.#call('setMessageReaction', {
|
|
209
|
+
chat_id: chatId,
|
|
210
|
+
message_id: messageId,
|
|
211
|
+
reaction: normalizedEmoji ? [{ type: 'emoji', emoji: normalizedEmoji }] : [],
|
|
212
|
+
}, { signal, timeoutMs });
|
|
213
|
+
}
|
|
214
|
+
|
|
206
215
|
async sendRichMessage({
|
|
207
216
|
chatId,
|
|
208
217
|
richMessage,
|
|
@@ -4,6 +4,7 @@ export const TELEGRAM_DESCRIPTOR = Object.freeze({
|
|
|
4
4
|
key: 'telegram',
|
|
5
5
|
label: 'Telegram',
|
|
6
6
|
connectionLabel: ' Bot API 长轮询',
|
|
7
|
+
reactions: Object.freeze({ processing: '👀', success: '👍', error: '👎' }),
|
|
7
8
|
});
|
|
8
9
|
|
|
9
10
|
export class TelegramHarnessBridge extends TextHarnessBridge {
|
|
@@ -168,6 +168,7 @@ export function normalizeTelegramUpdate(update, {
|
|
|
168
168
|
images: image ? [image] : [],
|
|
169
169
|
files: file ? [file] : [],
|
|
170
170
|
addressed,
|
|
171
|
+
reactionTarget: { chatId, messageId },
|
|
171
172
|
replyTarget: {
|
|
172
173
|
chatId,
|
|
173
174
|
chatType: message.chat.type,
|
|
@@ -303,6 +304,25 @@ export class TelegramBotClient {
|
|
|
303
304
|
return { providerMessageIds };
|
|
304
305
|
}
|
|
305
306
|
|
|
307
|
+
async addReaction(target, emoji, { signal } = {}) {
|
|
308
|
+
const reactionKey = String(emoji ?? '').trim();
|
|
309
|
+
await this.#api.setMessageReaction({
|
|
310
|
+
chatId: target.chatId,
|
|
311
|
+
messageId: target.messageId,
|
|
312
|
+
emoji: reactionKey,
|
|
313
|
+
signal: signal ?? this.#signal,
|
|
314
|
+
});
|
|
315
|
+
return reactionKey;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
removeReaction(target, _reactionKey, { signal } = {}) {
|
|
319
|
+
return this.#api.setMessageReaction({
|
|
320
|
+
chatId: target.chatId,
|
|
321
|
+
messageId: target.messageId,
|
|
322
|
+
signal: signal ?? this.#signal,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
306
326
|
sendTyping(target) {
|
|
307
327
|
return this.#api.sendChatAction({
|
|
308
328
|
chatId: target.chatId,
|
|
@@ -6,6 +6,7 @@ export const WHATSAPP_DESCRIPTOR = Object.freeze({
|
|
|
6
6
|
label: 'WhatsApp',
|
|
7
7
|
// Translated lazily: t() must run after setImHostLanguage, not at import time.
|
|
8
8
|
get connectionLabel() { return t(' Web 关联设备'); },
|
|
9
|
+
reactions: Object.freeze({ processing: '👀', success: '✅', error: '❌' }),
|
|
9
10
|
});
|
|
10
11
|
|
|
11
12
|
export class WhatsappHarnessBridge extends TextHarnessBridge {
|
|
@@ -246,6 +246,7 @@ export function normalizeWhatsappMessage(message, accountJid, {
|
|
|
246
246
|
addressed: !group || fromMe || mentioned || replyToSelf,
|
|
247
247
|
selfChat,
|
|
248
248
|
replyTarget: { jid: remoteJid, quoted: message, selfChat },
|
|
249
|
+
reactionTarget: { jid: remoteJid, key: message.key },
|
|
249
250
|
};
|
|
250
251
|
}
|
|
251
252
|
|
|
@@ -411,6 +412,41 @@ export class WhatsappBotClient {
|
|
|
411
412
|
}, 'image');
|
|
412
413
|
}
|
|
413
414
|
|
|
415
|
+
async addReaction(target, emoji, { signal } = {}) {
|
|
416
|
+
if (typeof emoji !== 'string' || !emoji.trim()) {
|
|
417
|
+
throw new TypeError('A WhatsApp reaction emoji is required');
|
|
418
|
+
}
|
|
419
|
+
const reactionKey = emoji.trim();
|
|
420
|
+
await this.#sendReaction(target, reactionKey, signal);
|
|
421
|
+
return reactionKey;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
removeReaction(target, _reactionKey, { signal } = {}) {
|
|
425
|
+
return this.#sendReaction(target, '', signal);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async #sendReaction(target, text, signal) {
|
|
429
|
+
if (typeof target?.jid !== 'string' || !target.jid || !target.key?.id) {
|
|
430
|
+
throw new TypeError('A WhatsApp reaction target is required');
|
|
431
|
+
}
|
|
432
|
+
const operationSignal = signal ?? this.#signal;
|
|
433
|
+
operationSignal?.throwIfAborted();
|
|
434
|
+
const messageId = randomBytes(10).toString('hex').toUpperCase();
|
|
435
|
+
if (typeof this.#outboundIds.reserve === 'function') {
|
|
436
|
+
this.#outboundIds.reserve(messageId);
|
|
437
|
+
} else {
|
|
438
|
+
this.#outboundIds.remember(messageId);
|
|
439
|
+
}
|
|
440
|
+
const pending = this.#socket.sendMessage(
|
|
441
|
+
target.jid,
|
|
442
|
+
{ react: { text, key: target.key } },
|
|
443
|
+
{ messageId },
|
|
444
|
+
);
|
|
445
|
+
const result = await waitWithSignal(pending, operationSignal);
|
|
446
|
+
this.#outboundIds.remember(result?.key?.id);
|
|
447
|
+
return result;
|
|
448
|
+
}
|
|
449
|
+
|
|
414
450
|
async #sendArtifact(target, file, content, presentation) {
|
|
415
451
|
this.#signal?.throwIfAborted();
|
|
416
452
|
await this.#stopTyping(target.jid);
|