@xmanrui/dsh-im 1.0.2 → 1.2.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 +23 -3
- package/README.md +23 -3
- package/assets/logo-dsh-im-chinese-readme-3x2.png +0 -0
- package/assets/logo_cn.png +0 -0
- package/lib/client.js +815 -560
- package/lib/index.js +163 -163
- package/package.json +1 -1
- package/plugin-src/client/agent-preset.js +15 -6
- package/plugin-src/client/channel-card-meta.js +48 -0
- package/plugin-src/client/channels/dingtalk/index.js +25 -19
- package/plugin-src/client/channels/dingtalk/styles.js +0 -6
- package/plugin-src/client/channels/feishu/index.js +41 -35
- package/plugin-src/client/channels/feishu/styles.js +0 -5
- package/plugin-src/client/channels/qq/index.js +24 -16
- package/plugin-src/client/channels/shared/token-channel.js +32 -24
- package/plugin-src/client/channels/wecom/index.js +24 -16
- package/plugin-src/client/channels/weixin/index.js +29 -23
- package/plugin-src/client/channels/weixin/styles.js +0 -5
- package/plugin-src/client/channels/whatsapp/api.js +11 -0
- package/plugin-src/client/channels/whatsapp/index.js +152 -23
- package/plugin-src/client/channels/whatsapp/styles.js +25 -0
- package/plugin-src/client/i18n.js +20 -0
- package/plugin-src/client/styles.js +23 -8
- package/plugin-src/host/channels/whatsapp/rpc.mjs +19 -1
- package/plugin-src/host/index.mjs +14 -1
- package/src/channels/dingtalk/dingtalk-api.mjs +215 -2
- package/src/channels/dingtalk/dingtalk-bridge.mjs +155 -4
- package/src/channels/discord/discord-api.mjs +134 -6
- package/src/channels/discord/discord-runtime.mjs +15 -4
- package/src/channels/feishu/bridge.mjs +223 -15
- package/src/channels/feishu/feishu-channel.mjs +227 -1
- package/src/channels/feishu/plugin-controller.mjs +1 -0
- package/src/channels/qq/qq-bridge.mjs +217 -10
- package/src/channels/shared/editable-message-stream.mjs +18 -1
- package/src/channels/shared/harness-client.mjs +99 -7
- package/src/channels/shared/semantic/artifact.mjs +748 -0
- package/src/channels/shared/semantic/delivery.mjs +153 -0
- package/src/channels/shared/text-harness-bridge.mjs +149 -3
- package/src/channels/shared/workspace-session.mjs +15 -1
- package/src/channels/slack/manifest.mjs +1 -0
- package/src/channels/slack/slack-api.mjs +167 -4
- package/src/channels/slack/slack-runtime.mjs +21 -5
- package/src/channels/telegram/telegram-api.mjs +111 -5
- package/src/channels/telegram/telegram-runtime.mjs +18 -4
- package/src/channels/wecom/wecom-bridge.mjs +260 -12
- package/src/channels/weixin/weixin-api.mjs +268 -2
- package/src/channels/weixin/weixin-bridge.mjs +134 -3
- package/src/channels/weixin/weixin-controller.mjs +5 -1
- package/src/channels/weixin/weixin-runtime.mjs +5 -1
- package/src/channels/whatsapp/config-store.mjs +43 -0
- package/src/channels/whatsapp/whatsapp-controller.mjs +22 -1
- package/src/channels/whatsapp/whatsapp-runtime.mjs +149 -5
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
areJidsSameUser,
|
|
3
5
|
downloadMediaMessage,
|
|
@@ -6,12 +8,18 @@ import {
|
|
|
6
8
|
|
|
7
9
|
import { splitMessageText } from '../shared/editable-message-stream.mjs';
|
|
8
10
|
import { ImagePromptError } from '../shared/image-prompt.mjs';
|
|
11
|
+
import { trackOutboundArtifactProviderPromise } from '../shared/semantic/artifact.mjs';
|
|
9
12
|
import { createWhatsappBridgeStatus, WhatsappHarnessBridge } from './whatsapp-bridge.mjs';
|
|
13
|
+
import {
|
|
14
|
+
WHATSAPP_ACCESS_MODES,
|
|
15
|
+
normalizeWhatsappAccessPolicy,
|
|
16
|
+
} from './config-store.mjs';
|
|
10
17
|
import { createWhatsappWebSession } from './whatsapp-web-session.mjs';
|
|
11
18
|
|
|
12
19
|
const IMAGE_MEDIA_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif']);
|
|
13
20
|
const DEFAULT_MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
14
21
|
const IMAGE_DOWNLOAD_TIMEOUT_MS = 15_000;
|
|
22
|
+
const WHATSAPP_MEDIA_UPLOAD_TIMEOUT_MS = 120_000;
|
|
15
23
|
const MESSAGE_WRAPPER_KEYS = [
|
|
16
24
|
'ephemeralMessage',
|
|
17
25
|
'viewOnceMessage',
|
|
@@ -177,6 +185,7 @@ export function normalizeWhatsappMessage(message, accountJid, {
|
|
|
177
185
|
&& [remoteJid, alternateRemoteJid].some((jid) => jid && areJidsSameUser(jid, accountJid));
|
|
178
186
|
if (fromMe && !selfChat) return null;
|
|
179
187
|
const senderJid = selfChat ? accountJid : group ? message.key.participant : remoteJid;
|
|
188
|
+
const senderAlternateJid = group ? message.key.participantAlt : alternateRemoteJid;
|
|
180
189
|
if (typeof senderJid !== 'string' || !senderJid) return null;
|
|
181
190
|
const viewOnce = hasViewOnceWrapper(message.message);
|
|
182
191
|
const content = normalizeMessageContent(message.message);
|
|
@@ -190,6 +199,7 @@ export function normalizeWhatsappMessage(message, accountJid, {
|
|
|
190
199
|
messageId: `${remoteJid}:${messageId}`,
|
|
191
200
|
providerMessageId: messageId,
|
|
192
201
|
senderId: senderJid,
|
|
202
|
+
senderAlternateId: typeof senderAlternateJid === 'string' ? senderAlternateJid : '',
|
|
193
203
|
senderIsBot: false,
|
|
194
204
|
kind: group ? 'group' : 'direct',
|
|
195
205
|
conversationId: remoteJid,
|
|
@@ -201,6 +211,22 @@ export function normalizeWhatsappMessage(message, accountJid, {
|
|
|
201
211
|
};
|
|
202
212
|
}
|
|
203
213
|
|
|
214
|
+
export function whatsappInboundAllowed(message, {
|
|
215
|
+
accessMode = WHATSAPP_ACCESS_MODES.selfOnly,
|
|
216
|
+
allowedNumbers = new Set(),
|
|
217
|
+
} = {}) {
|
|
218
|
+
if (accessMode === WHATSAPP_ACCESS_MODES.open) return true;
|
|
219
|
+
if (message?.kind !== 'direct') return false;
|
|
220
|
+
if (message.selfChat === true) return true;
|
|
221
|
+
if (accessMode !== WHATSAPP_ACCESS_MODES.privateAllowlist
|
|
222
|
+
|| !(allowedNumbers instanceof Set)) return false;
|
|
223
|
+
const senderJids = [message.senderId, message.senderAlternateId]
|
|
224
|
+
.filter((jid) => typeof jid === 'string' && jid.endsWith('@s.whatsapp.net'));
|
|
225
|
+
return [...allowedNumbers].some((number) => senderJids.some((jid) => (
|
|
226
|
+
areJidsSameUser(jid, `${number}@s.whatsapp.net`)
|
|
227
|
+
)));
|
|
228
|
+
}
|
|
229
|
+
|
|
204
230
|
class RecentWhatsappOutboundIds {
|
|
205
231
|
#ids = new Map();
|
|
206
232
|
|
|
@@ -225,27 +251,114 @@ class RecentWhatsappOutboundIds {
|
|
|
225
251
|
}
|
|
226
252
|
}
|
|
227
253
|
|
|
228
|
-
|
|
254
|
+
function abortReason(signal) {
|
|
255
|
+
return signal?.reason ?? new DOMException('The operation was aborted.', 'AbortError');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function waitWithSignal(promise, signal) {
|
|
259
|
+
if (!signal) return promise;
|
|
260
|
+
signal.throwIfAborted();
|
|
261
|
+
return new Promise((resolve, reject) => {
|
|
262
|
+
let settled = false;
|
|
263
|
+
const finish = (callback, value) => {
|
|
264
|
+
if (settled) return;
|
|
265
|
+
settled = true;
|
|
266
|
+
signal.removeEventListener('abort', onAbort);
|
|
267
|
+
callback(value);
|
|
268
|
+
};
|
|
269
|
+
const onAbort = () => finish(reject, abortReason(signal));
|
|
270
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
271
|
+
Promise.resolve(promise).then(
|
|
272
|
+
(value) => finish(resolve, value),
|
|
273
|
+
(error) => finish(reject, error),
|
|
274
|
+
);
|
|
275
|
+
if (signal.aborted) onAbort();
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function uncertainArtifactDelivery(error) {
|
|
280
|
+
if (error?.code === 'artifact-delivery-uncertain') return error;
|
|
281
|
+
const uncertain = new Error('WhatsApp could not confirm result file delivery.');
|
|
282
|
+
uncertain.code = 'artifact-delivery-uncertain';
|
|
283
|
+
uncertain.cause = error;
|
|
284
|
+
return uncertain;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export class WhatsappBotClient {
|
|
229
288
|
#socket;
|
|
230
289
|
#outboundIds;
|
|
290
|
+
#signal;
|
|
291
|
+
#mediaUploadTimeoutMs;
|
|
231
292
|
#typingTimers = new Map();
|
|
232
293
|
|
|
233
|
-
constructor(socket, outboundIds
|
|
294
|
+
constructor(socket, outboundIds, {
|
|
295
|
+
signal,
|
|
296
|
+
mediaUploadTimeoutMs = WHATSAPP_MEDIA_UPLOAD_TIMEOUT_MS,
|
|
297
|
+
} = {}) {
|
|
234
298
|
this.#socket = socket;
|
|
235
299
|
this.#outboundIds = outboundIds;
|
|
300
|
+
this.#signal = signal;
|
|
301
|
+
this.#mediaUploadTimeoutMs = mediaUploadTimeoutMs;
|
|
236
302
|
}
|
|
237
303
|
|
|
238
304
|
async sendText(target, text) {
|
|
239
305
|
await this.#stopTyping(target.jid);
|
|
240
|
-
|
|
306
|
+
const providerMessageIds = [];
|
|
241
307
|
for (const [index, chunk] of splitMessageText(text, 4_000).entries()) {
|
|
242
|
-
result = await this.#socket.sendMessage(
|
|
308
|
+
const result = await this.#socket.sendMessage(
|
|
243
309
|
target.jid,
|
|
244
310
|
{ text: chunk },
|
|
245
311
|
index === 0 && target.quoted ? { quoted: target.quoted } : undefined,
|
|
246
312
|
);
|
|
247
313
|
this.#outboundIds.remember(result?.key?.id);
|
|
314
|
+
if (typeof result?.key?.id === 'string' && result.key.id) {
|
|
315
|
+
providerMessageIds.push(result.key.id);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return { providerMessageIds };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async sendFile(target, file) {
|
|
322
|
+
this.#signal?.throwIfAborted();
|
|
323
|
+
await this.#stopTyping(target.jid);
|
|
324
|
+
this.#signal?.throwIfAborted();
|
|
325
|
+
const deliverySeed = typeof file.deliveryKey === 'string' && file.deliveryKey
|
|
326
|
+
? file.deliveryKey
|
|
327
|
+
: file.artifactId;
|
|
328
|
+
const messageId = typeof deliverySeed === 'string' && deliverySeed
|
|
329
|
+
? createHash('sha256').update(deliverySeed).digest('hex').slice(0, 20).toUpperCase()
|
|
330
|
+
: undefined;
|
|
331
|
+
const options = {
|
|
332
|
+
...(target.quoted ? { quoted: target.quoted } : {}),
|
|
333
|
+
...(messageId ? { messageId } : {}),
|
|
334
|
+
mediaUploadTimeoutMs: this.#mediaUploadTimeoutMs,
|
|
335
|
+
};
|
|
336
|
+
// Baileys can emit a self-chat echo before sendMessage settles. Reserve the
|
|
337
|
+
// deterministic id before dispatch so that echo cannot re-enter the bridge.
|
|
338
|
+
this.#outboundIds.remember(messageId);
|
|
339
|
+
let result;
|
|
340
|
+
try {
|
|
341
|
+
const pending = this.#socket.sendMessage(
|
|
342
|
+
target.jid,
|
|
343
|
+
{
|
|
344
|
+
document: file.bytes,
|
|
345
|
+
mimetype: file.mediaType ?? 'application/octet-stream',
|
|
346
|
+
fileName: file.fileName,
|
|
347
|
+
},
|
|
348
|
+
options,
|
|
349
|
+
);
|
|
350
|
+
trackOutboundArtifactProviderPromise(file, pending);
|
|
351
|
+
const timeout = AbortSignal.timeout(this.#mediaUploadTimeoutMs);
|
|
352
|
+
const waitSignal = this.#signal
|
|
353
|
+
? AbortSignal.any([this.#signal, timeout])
|
|
354
|
+
: timeout;
|
|
355
|
+
result = await waitWithSignal(pending, waitSignal);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
if (this.#signal?.aborted) throw abortReason(this.#signal);
|
|
358
|
+
throw uncertainArtifactDelivery(error);
|
|
248
359
|
}
|
|
360
|
+
this.#signal?.throwIfAborted();
|
|
361
|
+
this.#outboundIds.remember(result?.key?.id);
|
|
249
362
|
return result;
|
|
250
363
|
}
|
|
251
364
|
|
|
@@ -298,6 +411,9 @@ export class WhatsappRuntime {
|
|
|
298
411
|
#logger;
|
|
299
412
|
#replyTimeoutMs;
|
|
300
413
|
#connectTimeoutMs;
|
|
414
|
+
#mediaUploadTimeoutMs;
|
|
415
|
+
#accessMode;
|
|
416
|
+
#allowedPrivateNumbers;
|
|
301
417
|
#createSession;
|
|
302
418
|
#status = createWhatsappRuntimeStatus();
|
|
303
419
|
#abortController = null;
|
|
@@ -314,6 +430,7 @@ export class WhatsappRuntime {
|
|
|
314
430
|
logger = console,
|
|
315
431
|
replyTimeoutMs = 600_000,
|
|
316
432
|
connectTimeoutMs = 30_000,
|
|
433
|
+
mediaUploadTimeoutMs = WHATSAPP_MEDIA_UPLOAD_TIMEOUT_MS,
|
|
317
434
|
createSession = createWhatsappWebSession,
|
|
318
435
|
}) {
|
|
319
436
|
if (!config || !authDir || !harness || !state || typeof createSession !== 'function') {
|
|
@@ -326,13 +443,29 @@ export class WhatsappRuntime {
|
|
|
326
443
|
this.#logger = logger;
|
|
327
444
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
328
445
|
this.#connectTimeoutMs = connectTimeoutMs;
|
|
446
|
+
if (!Number.isSafeInteger(mediaUploadTimeoutMs) || mediaUploadTimeoutMs <= 0) {
|
|
447
|
+
throw new TypeError('mediaUploadTimeoutMs must be a positive safe integer');
|
|
448
|
+
}
|
|
449
|
+
this.#mediaUploadTimeoutMs = Math.min(
|
|
450
|
+
mediaUploadTimeoutMs,
|
|
451
|
+
WHATSAPP_MEDIA_UPLOAD_TIMEOUT_MS,
|
|
452
|
+
);
|
|
329
453
|
this.#createSession = createSession;
|
|
454
|
+
this.setAccessPolicy(config);
|
|
330
455
|
}
|
|
331
456
|
|
|
332
457
|
get status() {
|
|
333
458
|
return structuredClone(this.#status);
|
|
334
459
|
}
|
|
335
460
|
|
|
461
|
+
setAccessPolicy(value) {
|
|
462
|
+
const policy = normalizeWhatsappAccessPolicy(value);
|
|
463
|
+
this.#accessMode = policy.accessMode;
|
|
464
|
+
this.#allowedPrivateNumbers = new Set(policy.allowedNumbers);
|
|
465
|
+
this.#config = { ...this.#config, ...policy };
|
|
466
|
+
return policy;
|
|
467
|
+
}
|
|
468
|
+
|
|
336
469
|
async start() {
|
|
337
470
|
if (this.#status.ready && this.#session) return this.status;
|
|
338
471
|
if (this.#starting) return this.#starting;
|
|
@@ -366,6 +499,14 @@ export class WhatsappRuntime {
|
|
|
366
499
|
const message = normalizeWhatsappMessage(raw, this.#config.accountJid);
|
|
367
500
|
if (!message || outboundIds.has(message.providerMessageId) || !this.#bridge) return;
|
|
368
501
|
this.#status.lastCheckedAt = Date.now();
|
|
502
|
+
if (!whatsappInboundAllowed(message, {
|
|
503
|
+
accessMode: this.#accessMode,
|
|
504
|
+
allowedNumbers: this.#allowedPrivateNumbers,
|
|
505
|
+
})) {
|
|
506
|
+
this.#status.messagesRejected += 1;
|
|
507
|
+
this.#status.lastRejectedAt = new Date().toISOString();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
369
510
|
await this.#bridge.accept(message);
|
|
370
511
|
},
|
|
371
512
|
onDisconnect: ({ error }) => {
|
|
@@ -390,7 +531,10 @@ export class WhatsappRuntime {
|
|
|
390
531
|
if (!areJidsSameUser(identity.accountJid, this.#config.accountJid)) {
|
|
391
532
|
throw new Error('WhatsApp linked account does not match the saved bot');
|
|
392
533
|
}
|
|
393
|
-
const client = new WhatsappBotClient(session.socket, outboundIds
|
|
534
|
+
const client = new WhatsappBotClient(session.socket, outboundIds, {
|
|
535
|
+
signal: controller.signal,
|
|
536
|
+
mediaUploadTimeoutMs: this.#mediaUploadTimeoutMs,
|
|
537
|
+
});
|
|
394
538
|
this.#client = client;
|
|
395
539
|
this.#bridge = new WhatsappHarnessBridge({
|
|
396
540
|
bot: client,
|