@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.
Files changed (52) hide show
  1. package/README.en.md +23 -3
  2. package/README.md +23 -3
  3. package/assets/logo-dsh-im-chinese-readme-3x2.png +0 -0
  4. package/assets/logo_cn.png +0 -0
  5. package/lib/client.js +815 -560
  6. package/lib/index.js +163 -163
  7. package/package.json +1 -1
  8. package/plugin-src/client/agent-preset.js +15 -6
  9. package/plugin-src/client/channel-card-meta.js +48 -0
  10. package/plugin-src/client/channels/dingtalk/index.js +25 -19
  11. package/plugin-src/client/channels/dingtalk/styles.js +0 -6
  12. package/plugin-src/client/channels/feishu/index.js +41 -35
  13. package/plugin-src/client/channels/feishu/styles.js +0 -5
  14. package/plugin-src/client/channels/qq/index.js +24 -16
  15. package/plugin-src/client/channels/shared/token-channel.js +32 -24
  16. package/plugin-src/client/channels/wecom/index.js +24 -16
  17. package/plugin-src/client/channels/weixin/index.js +29 -23
  18. package/plugin-src/client/channels/weixin/styles.js +0 -5
  19. package/plugin-src/client/channels/whatsapp/api.js +11 -0
  20. package/plugin-src/client/channels/whatsapp/index.js +152 -23
  21. package/plugin-src/client/channels/whatsapp/styles.js +25 -0
  22. package/plugin-src/client/i18n.js +20 -0
  23. package/plugin-src/client/styles.js +23 -8
  24. package/plugin-src/host/channels/whatsapp/rpc.mjs +19 -1
  25. package/plugin-src/host/index.mjs +14 -1
  26. package/src/channels/dingtalk/dingtalk-api.mjs +215 -2
  27. package/src/channels/dingtalk/dingtalk-bridge.mjs +155 -4
  28. package/src/channels/discord/discord-api.mjs +134 -6
  29. package/src/channels/discord/discord-runtime.mjs +15 -4
  30. package/src/channels/feishu/bridge.mjs +223 -15
  31. package/src/channels/feishu/feishu-channel.mjs +227 -1
  32. package/src/channels/feishu/plugin-controller.mjs +1 -0
  33. package/src/channels/qq/qq-bridge.mjs +217 -10
  34. package/src/channels/shared/editable-message-stream.mjs +18 -1
  35. package/src/channels/shared/harness-client.mjs +99 -7
  36. package/src/channels/shared/semantic/artifact.mjs +748 -0
  37. package/src/channels/shared/semantic/delivery.mjs +153 -0
  38. package/src/channels/shared/text-harness-bridge.mjs +149 -3
  39. package/src/channels/shared/workspace-session.mjs +15 -1
  40. package/src/channels/slack/manifest.mjs +1 -0
  41. package/src/channels/slack/slack-api.mjs +167 -4
  42. package/src/channels/slack/slack-runtime.mjs +21 -5
  43. package/src/channels/telegram/telegram-api.mjs +111 -5
  44. package/src/channels/telegram/telegram-runtime.mjs +18 -4
  45. package/src/channels/wecom/wecom-bridge.mjs +260 -12
  46. package/src/channels/weixin/weixin-api.mjs +268 -2
  47. package/src/channels/weixin/weixin-bridge.mjs +134 -3
  48. package/src/channels/weixin/weixin-controller.mjs +5 -1
  49. package/src/channels/weixin/weixin-runtime.mjs +5 -1
  50. package/src/channels/whatsapp/config-store.mjs +43 -0
  51. package/src/channels/whatsapp/whatsapp-controller.mjs +22 -1
  52. 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
- class WhatsappBotClient {
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
- let result = null;
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,