@xmanrui/dsh-im 1.0.2 → 1.1.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 (47) hide show
  1. package/README.en.md +18 -0
  2. package/README.md +18 -0
  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 +603 -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/index.js +27 -23
  20. package/plugin-src/client/i18n.js +2 -0
  21. package/plugin-src/client/styles.js +23 -8
  22. package/plugin-src/host/index.mjs +14 -1
  23. package/src/channels/dingtalk/dingtalk-api.mjs +215 -2
  24. package/src/channels/dingtalk/dingtalk-bridge.mjs +155 -4
  25. package/src/channels/discord/discord-api.mjs +134 -6
  26. package/src/channels/discord/discord-runtime.mjs +15 -4
  27. package/src/channels/feishu/bridge.mjs +223 -15
  28. package/src/channels/feishu/feishu-channel.mjs +227 -1
  29. package/src/channels/feishu/plugin-controller.mjs +1 -0
  30. package/src/channels/qq/qq-bridge.mjs +217 -10
  31. package/src/channels/shared/editable-message-stream.mjs +18 -1
  32. package/src/channels/shared/harness-client.mjs +99 -7
  33. package/src/channels/shared/semantic/artifact.mjs +748 -0
  34. package/src/channels/shared/semantic/delivery.mjs +153 -0
  35. package/src/channels/shared/text-harness-bridge.mjs +149 -3
  36. package/src/channels/shared/workspace-session.mjs +15 -1
  37. package/src/channels/slack/manifest.mjs +1 -0
  38. package/src/channels/slack/slack-api.mjs +167 -4
  39. package/src/channels/slack/slack-runtime.mjs +21 -5
  40. package/src/channels/telegram/telegram-api.mjs +111 -5
  41. package/src/channels/telegram/telegram-runtime.mjs +18 -4
  42. package/src/channels/wecom/wecom-bridge.mjs +260 -12
  43. package/src/channels/weixin/weixin-api.mjs +268 -2
  44. package/src/channels/weixin/weixin-bridge.mjs +134 -3
  45. package/src/channels/weixin/weixin-controller.mjs +5 -1
  46. package/src/channels/weixin/weixin-runtime.mjs +5 -1
  47. package/src/channels/whatsapp/whatsapp-runtime.mjs +108 -5
@@ -1,3 +1,5 @@
1
+ import { createHash } from 'node:crypto';
2
+
1
3
  import {
2
4
  areJidsSameUser,
3
5
  downloadMediaMessage,
@@ -6,12 +8,14 @@ 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';
10
13
  import { createWhatsappWebSession } from './whatsapp-web-session.mjs';
11
14
 
12
15
  const IMAGE_MEDIA_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif']);
13
16
  const DEFAULT_MAX_IMAGE_BYTES = 5 * 1024 * 1024;
14
17
  const IMAGE_DOWNLOAD_TIMEOUT_MS = 15_000;
18
+ const WHATSAPP_MEDIA_UPLOAD_TIMEOUT_MS = 120_000;
15
19
  const MESSAGE_WRAPPER_KEYS = [
16
20
  'ephemeralMessage',
17
21
  'viewOnceMessage',
@@ -225,27 +229,114 @@ class RecentWhatsappOutboundIds {
225
229
  }
226
230
  }
227
231
 
228
- class WhatsappBotClient {
232
+ function abortReason(signal) {
233
+ return signal?.reason ?? new DOMException('The operation was aborted.', 'AbortError');
234
+ }
235
+
236
+ function waitWithSignal(promise, signal) {
237
+ if (!signal) return promise;
238
+ signal.throwIfAborted();
239
+ return new Promise((resolve, reject) => {
240
+ let settled = false;
241
+ const finish = (callback, value) => {
242
+ if (settled) return;
243
+ settled = true;
244
+ signal.removeEventListener('abort', onAbort);
245
+ callback(value);
246
+ };
247
+ const onAbort = () => finish(reject, abortReason(signal));
248
+ signal.addEventListener('abort', onAbort, { once: true });
249
+ Promise.resolve(promise).then(
250
+ (value) => finish(resolve, value),
251
+ (error) => finish(reject, error),
252
+ );
253
+ if (signal.aborted) onAbort();
254
+ });
255
+ }
256
+
257
+ function uncertainArtifactDelivery(error) {
258
+ if (error?.code === 'artifact-delivery-uncertain') return error;
259
+ const uncertain = new Error('WhatsApp could not confirm result file delivery.');
260
+ uncertain.code = 'artifact-delivery-uncertain';
261
+ uncertain.cause = error;
262
+ return uncertain;
263
+ }
264
+
265
+ export class WhatsappBotClient {
229
266
  #socket;
230
267
  #outboundIds;
268
+ #signal;
269
+ #mediaUploadTimeoutMs;
231
270
  #typingTimers = new Map();
232
271
 
233
- constructor(socket, outboundIds) {
272
+ constructor(socket, outboundIds, {
273
+ signal,
274
+ mediaUploadTimeoutMs = WHATSAPP_MEDIA_UPLOAD_TIMEOUT_MS,
275
+ } = {}) {
234
276
  this.#socket = socket;
235
277
  this.#outboundIds = outboundIds;
278
+ this.#signal = signal;
279
+ this.#mediaUploadTimeoutMs = mediaUploadTimeoutMs;
236
280
  }
237
281
 
238
282
  async sendText(target, text) {
239
283
  await this.#stopTyping(target.jid);
240
- let result = null;
284
+ const providerMessageIds = [];
241
285
  for (const [index, chunk] of splitMessageText(text, 4_000).entries()) {
242
- result = await this.#socket.sendMessage(
286
+ const result = await this.#socket.sendMessage(
243
287
  target.jid,
244
288
  { text: chunk },
245
289
  index === 0 && target.quoted ? { quoted: target.quoted } : undefined,
246
290
  );
247
291
  this.#outboundIds.remember(result?.key?.id);
292
+ if (typeof result?.key?.id === 'string' && result.key.id) {
293
+ providerMessageIds.push(result.key.id);
294
+ }
248
295
  }
296
+ return { providerMessageIds };
297
+ }
298
+
299
+ async sendFile(target, file) {
300
+ this.#signal?.throwIfAborted();
301
+ await this.#stopTyping(target.jid);
302
+ this.#signal?.throwIfAborted();
303
+ const deliverySeed = typeof file.deliveryKey === 'string' && file.deliveryKey
304
+ ? file.deliveryKey
305
+ : file.artifactId;
306
+ const messageId = typeof deliverySeed === 'string' && deliverySeed
307
+ ? createHash('sha256').update(deliverySeed).digest('hex').slice(0, 20).toUpperCase()
308
+ : undefined;
309
+ const options = {
310
+ ...(target.quoted ? { quoted: target.quoted } : {}),
311
+ ...(messageId ? { messageId } : {}),
312
+ mediaUploadTimeoutMs: this.#mediaUploadTimeoutMs,
313
+ };
314
+ // Baileys can emit a self-chat echo before sendMessage settles. Reserve the
315
+ // deterministic id before dispatch so that echo cannot re-enter the bridge.
316
+ this.#outboundIds.remember(messageId);
317
+ let result;
318
+ try {
319
+ const pending = this.#socket.sendMessage(
320
+ target.jid,
321
+ {
322
+ document: file.bytes,
323
+ mimetype: file.mediaType ?? 'application/octet-stream',
324
+ fileName: file.fileName,
325
+ },
326
+ options,
327
+ );
328
+ trackOutboundArtifactProviderPromise(file, pending);
329
+ const timeout = AbortSignal.timeout(this.#mediaUploadTimeoutMs);
330
+ const waitSignal = this.#signal
331
+ ? AbortSignal.any([this.#signal, timeout])
332
+ : timeout;
333
+ result = await waitWithSignal(pending, waitSignal);
334
+ } catch (error) {
335
+ if (this.#signal?.aborted) throw abortReason(this.#signal);
336
+ throw uncertainArtifactDelivery(error);
337
+ }
338
+ this.#signal?.throwIfAborted();
339
+ this.#outboundIds.remember(result?.key?.id);
249
340
  return result;
250
341
  }
251
342
 
@@ -298,6 +389,7 @@ export class WhatsappRuntime {
298
389
  #logger;
299
390
  #replyTimeoutMs;
300
391
  #connectTimeoutMs;
392
+ #mediaUploadTimeoutMs;
301
393
  #createSession;
302
394
  #status = createWhatsappRuntimeStatus();
303
395
  #abortController = null;
@@ -314,6 +406,7 @@ export class WhatsappRuntime {
314
406
  logger = console,
315
407
  replyTimeoutMs = 600_000,
316
408
  connectTimeoutMs = 30_000,
409
+ mediaUploadTimeoutMs = WHATSAPP_MEDIA_UPLOAD_TIMEOUT_MS,
317
410
  createSession = createWhatsappWebSession,
318
411
  }) {
319
412
  if (!config || !authDir || !harness || !state || typeof createSession !== 'function') {
@@ -326,6 +419,13 @@ export class WhatsappRuntime {
326
419
  this.#logger = logger;
327
420
  this.#replyTimeoutMs = replyTimeoutMs;
328
421
  this.#connectTimeoutMs = connectTimeoutMs;
422
+ if (!Number.isSafeInteger(mediaUploadTimeoutMs) || mediaUploadTimeoutMs <= 0) {
423
+ throw new TypeError('mediaUploadTimeoutMs must be a positive safe integer');
424
+ }
425
+ this.#mediaUploadTimeoutMs = Math.min(
426
+ mediaUploadTimeoutMs,
427
+ WHATSAPP_MEDIA_UPLOAD_TIMEOUT_MS,
428
+ );
329
429
  this.#createSession = createSession;
330
430
  }
331
431
 
@@ -390,7 +490,10 @@ export class WhatsappRuntime {
390
490
  if (!areJidsSameUser(identity.accountJid, this.#config.accountJid)) {
391
491
  throw new Error('WhatsApp linked account does not match the saved bot');
392
492
  }
393
- const client = new WhatsappBotClient(session.socket, outboundIds);
493
+ const client = new WhatsappBotClient(session.socket, outboundIds, {
494
+ signal: controller.signal,
495
+ mediaUploadTimeoutMs: this.#mediaUploadTimeoutMs,
496
+ });
394
497
  this.#client = client;
395
498
  this.#bridge = new WhatsappHarnessBridge({
396
499
  bot: client,