@meet-im/meet-bot-jssdk 0.0.6 → 1.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/dist/index.cjs CHANGED
@@ -1,5 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ var crypto = require('crypto');
4
+
5
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
6
+
7
+ var crypto__default = /*#__PURE__*/_interopDefault(crypto);
8
+
3
9
  // @meet/meet-bot-jssdk - MeetIM Chatbot JavaScript SDK
4
10
  var __defProp = Object.defineProperty;
5
11
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -92,7 +98,9 @@ var HTTP = {
92
98
  /** 请求超时时间(毫秒),默认 60000ms */
93
99
  DEFAULT_TIMEOUT: 6e4,
94
100
  /** 长轮询额外超时缓冲时间(毫秒),默认 10000ms */
95
- POLLING_TIMEOUT_BUFFER: 1e4
101
+ POLLING_TIMEOUT_BUFFER: 1e4,
102
+ /** 上传超时时间(毫秒),默认 1 小时 */
103
+ UPLOAD_TIMEOUT: 3600 * 1e3
96
104
  };
97
105
  var API = {
98
106
  /** getUpdates 默认超时时间(秒),默认 30(long polling) */
@@ -100,6 +108,64 @@ var API = {
100
108
  /** getUpdates 默认拉取条数,默认 100 条 */
101
109
  DEFAULT_LIMIT: 100
102
110
  };
111
+ var UPLOAD = {
112
+ /** 分片上传阈值,>= 5MB 使用分片上传 */
113
+ MULTIPART_THRESHOLD: 5 * 1024 * 1024,
114
+ /** 分片上传最大重试次数 */
115
+ MAX_RETRY_COUNT: 3,
116
+ /** 分片上传重试间隔(毫秒) */
117
+ RETRY_DELAY: 1e3,
118
+ /** 最大并发上传数 */
119
+ MAX_CONCURRENCY: 4
120
+ };
121
+ var CHUNK_RULES = [
122
+ { maxSize: 1 * 1024 * 1024, chunks: 1 },
123
+ // < 1MB: 1 片
124
+ { maxSize: 20 * 1024 * 1024, chunks: 5 },
125
+ // 1-20MB: 5 片
126
+ { maxSize: 100 * 1024 * 1024, chunks: 20 },
127
+ // 20-100MB: 20 片
128
+ { maxSize: Infinity, chunks: 50 }
129
+ // > 100MB: 50 片
130
+ ];
131
+ var SESSION_TYPE = {
132
+ PRIVATE: 1,
133
+ GROUP: 3,
134
+ CHANNEL: 4
135
+ };
136
+ function getChunkNum(size) {
137
+ for (const rule of CHUNK_RULES) {
138
+ if (size < rule.maxSize) {
139
+ return rule.chunks;
140
+ }
141
+ }
142
+ return 50;
143
+ }
144
+ function getConvID(firstID, secondID, sessionType, companyID = 1) {
145
+ let convID;
146
+ if (sessionType === SESSION_TYPE.PRIVATE) {
147
+ const min = Math.min(firstID, secondID);
148
+ const max = Math.max(firstID, secondID);
149
+ convID = `${min}:${max}`;
150
+ if (companyID > 1) {
151
+ convID += `:${companyID}`;
152
+ }
153
+ } else if (sessionType === SESSION_TYPE.GROUP) {
154
+ convID = `${firstID}+${secondID}`;
155
+ if (companyID > 1) {
156
+ convID += `+${companyID}`;
157
+ }
158
+ } else {
159
+ convID = `${firstID}_${secondID}`;
160
+ if (companyID > 1) {
161
+ convID += `_${companyID}`;
162
+ }
163
+ }
164
+ return convID;
165
+ }
166
+ function getQuoteMsgKey(convID, seqID) {
167
+ return `${convID}:${seqID}`;
168
+ }
103
169
 
104
170
  // src/utils/logger.ts
105
171
  var Logger = class {
@@ -175,7 +241,19 @@ async function request(options) {
175
241
  responseHeaders[key] = value;
176
242
  });
177
243
  logger.info("[Response Status]", response.status);
178
- const data = await response.json();
244
+ const contentType = response.headers.get("content-type") || "";
245
+ const responseText = await response.text();
246
+ if (!contentType.includes("application/json")) {
247
+ logger.error("[Response] Non-JSON response:", responseText.substring(0, 500));
248
+ throw new exports.NetworkError(`Server returned non-JSON response (status ${response.status})`, void 0);
249
+ }
250
+ let data;
251
+ try {
252
+ data = JSON.parse(responseText);
253
+ } catch {
254
+ logger.error("[Response] Failed to parse JSON:", responseText.substring(0, 500));
255
+ throw new exports.NetworkError("Failed to parse server response", void 0);
256
+ }
179
257
  logger.info("[Response Body]", JSON.stringify(data));
180
258
  if (!response.ok || !data.ok) {
181
259
  const errorData = data;
@@ -220,6 +298,255 @@ function mapStatusCodeToErrorCode(status) {
220
298
  }
221
299
  }
222
300
 
301
+ // src/api/file.ts
302
+ async function getUploadURL(params) {
303
+ const { token, baseUrl, ...body } = params;
304
+ return request({
305
+ token,
306
+ baseUrl,
307
+ method: "POST",
308
+ path: "getUploadURL",
309
+ body: {
310
+ originFileName: body.originFileName,
311
+ contentType: body.contentType,
312
+ md5: body.md5,
313
+ size: body.size,
314
+ fullImage: body.fullImage,
315
+ videoLength: body.videoLength,
316
+ bestDomain: body.bestDomain,
317
+ uploadId: body.uploadId,
318
+ chunkNum: body.chunkNum
319
+ },
320
+ timeout: HTTP.UPLOAD_TIMEOUT
321
+ });
322
+ }
323
+ async function getMultiPartUploadURL(params) {
324
+ const { token, baseUrl, ...body } = params;
325
+ return request({
326
+ token,
327
+ baseUrl,
328
+ method: "POST",
329
+ path: "getMultiPartUploadURL",
330
+ body: {
331
+ originFileName: body.originFileName,
332
+ contentType: body.contentType,
333
+ md5: body.md5,
334
+ size: body.size,
335
+ fullImage: body.fullImage,
336
+ videoLength: body.videoLength,
337
+ bestDomain: body.bestDomain
338
+ },
339
+ timeout: HTTP.UPLOAD_TIMEOUT
340
+ });
341
+ }
342
+ async function completeMultipartUpload(params) {
343
+ const { token, baseUrl, ...body } = params;
344
+ return request({
345
+ token,
346
+ baseUrl,
347
+ method: "POST",
348
+ path: "completeMultipartUpload",
349
+ body: {
350
+ originFileName: body.originFileName,
351
+ md5: body.md5,
352
+ UploadParts: body.UploadParts
353
+ },
354
+ timeout: HTTP.UPLOAD_TIMEOUT
355
+ });
356
+ }
357
+ async function getAccessURL(params) {
358
+ const { token, baseUrl, ...queryParams } = params;
359
+ return request({
360
+ token,
361
+ baseUrl,
362
+ method: "GET",
363
+ path: "getAccessURL",
364
+ params: {
365
+ firstId: queryParams.firstId,
366
+ secondId: queryParams.secondId,
367
+ sessionType: queryParams.sessionType,
368
+ seqId: queryParams.seqId,
369
+ fileId: queryParams.fileId,
370
+ companyId: queryParams.companyId,
371
+ "x-oss-process": queryParams["x-oss-process"],
372
+ printResult: queryParams.printResult ?? "1"
373
+ }
374
+ });
375
+ }
376
+ async function computeMD5(buffer) {
377
+ return crypto__default.default.createHash("md5").update(buffer).digest("hex");
378
+ }
379
+ async function uploadToOSS(signedUrl, buffer, contentType, callback, onProgress) {
380
+ const startTime = Date.now();
381
+ const total = buffer.length;
382
+ const response = await fetch(signedUrl, {
383
+ method: "PUT",
384
+ headers: {
385
+ "Content-Type": contentType,
386
+ "X-Oss-Callback": callback
387
+ },
388
+ body: buffer
389
+ });
390
+ if (!response.ok) {
391
+ const errorBody = await response.text().catch(() => "");
392
+ logger.error(`OSS upload failed: ${response.status} ${response.statusText}`);
393
+ logger.error(`OSS error body: ${errorBody}`);
394
+ throw new Error(`OSS upload failed: ${response.status} - ${errorBody}`);
395
+ }
396
+ const responseText = await response.text();
397
+ logger.info(`[uploadToOSS] OSS response status: ${response.status}`);
398
+ logger.info(`[uploadToOSS] OSS response: ${responseText}`);
399
+ let result;
400
+ try {
401
+ const parsed = JSON.parse(responseText);
402
+ result = {
403
+ id: parsed.id || parsed.ID || 0,
404
+ path: parsed.path || "",
405
+ size: parsed.size || 0
406
+ };
407
+ } catch {
408
+ const idMatch = responseText.match(/<id>(\d+)<\/id>/i);
409
+ const pathMatch = responseText.match(/<path>([^<]*)<\/path>/i);
410
+ const sizeMatch = responseText.match(/<size>(\d+)<\/size>/i);
411
+ result = {
412
+ id: idMatch?.[1] ? parseInt(idMatch[1], 10) : 0,
413
+ path: pathMatch?.[1] || "",
414
+ size: sizeMatch?.[1] ? parseInt(sizeMatch[1], 10) : 0
415
+ };
416
+ }
417
+ if (onProgress) {
418
+ const seconds = Math.round((Date.now() - startTime) / 1e3) || 1;
419
+ const speedPerSecond = formatSpeed(total / seconds);
420
+ onProgress({
421
+ percent: "100%",
422
+ loaded: total,
423
+ total,
424
+ speedPerSecond,
425
+ percentRate: 1
426
+ });
427
+ }
428
+ return result;
429
+ }
430
+ function formatSpeed(bytesPerSecond) {
431
+ if (bytesPerSecond < 1024) {
432
+ return `${bytesPerSecond.toFixed(0)}B/s`;
433
+ } else if (bytesPerSecond < 1024 * 1024) {
434
+ return `${(bytesPerSecond / 1024).toFixed(2)}KB/s`;
435
+ } else {
436
+ return `${(bytesPerSecond / 1024 / 1024).toFixed(2)}MB/s`;
437
+ }
438
+ }
439
+ async function uploadFile(token, buffer, options, baseUrl) {
440
+ const { fileName, contentType, onProgress } = options;
441
+ const size = buffer.length;
442
+ const md5 = await computeMD5(buffer);
443
+ logger.info(`[uploadFile] Starting upload: ${fileName}, size: ${size}, md5: ${md5}`);
444
+ if (size < UPLOAD.MULTIPART_THRESHOLD) {
445
+ return uploadSingleFile(token, buffer, fileName, contentType, md5, baseUrl, onProgress);
446
+ } else {
447
+ return uploadMultipartFile(token, buffer, fileName, contentType, md5, baseUrl, onProgress);
448
+ }
449
+ }
450
+ async function uploadSingleFile(token, buffer, fileName, contentType, md5, baseUrl, onProgress) {
451
+ const result = await getUploadURL({
452
+ token,
453
+ baseUrl,
454
+ originFileName: fileName,
455
+ contentType,
456
+ md5,
457
+ size: buffer.length
458
+ });
459
+ if (result.id > 0 && !result.signedUrl) {
460
+ logger.info(`[uploadFile] Instant upload (MD5 match): ${result.id}`);
461
+ return { fileID: result.id, path: result.path, size: result.size };
462
+ }
463
+ const ossResult = await uploadToOSS(result.signedUrl, buffer, contentType, result.callback || "", onProgress);
464
+ return { fileID: ossResult.id, path: ossResult.path, size: ossResult.size };
465
+ }
466
+ async function uploadMultipartFile(token, buffer, fileName, contentType, md5, baseUrl, onProgress) {
467
+ const size = buffer.length;
468
+ const chunkNum = getChunkNum(size);
469
+ logger.info(`[uploadMultipartFile] Starting multipart upload: ${chunkNum} chunks`);
470
+ const result = await getMultiPartUploadURL({
471
+ token,
472
+ baseUrl,
473
+ originFileName: fileName,
474
+ contentType,
475
+ md5,
476
+ size
477
+ });
478
+ if (result.ID && result.ID > 0) {
479
+ logger.info(`[uploadMultipartFile] Instant upload (MD5 match): ${result.ID}`);
480
+ return { fileID: result.ID, path: result.path, size: result.size };
481
+ }
482
+ const uploadParts = [];
483
+ const chunkSize = Math.ceil(size / chunkNum);
484
+ const startTime = Date.now();
485
+ let totalUploaded = 0;
486
+ for (let i = 1; i <= chunkNum; i++) {
487
+ const start = (i - 1) * chunkSize;
488
+ const end = Math.min(start + chunkSize, size);
489
+ const chunk = buffer.subarray(start, end);
490
+ const signedUrl = result.mapSignURLs[String(i)];
491
+ if (!signedUrl) {
492
+ throw new Error(`Missing signed URL for chunk ${i}`);
493
+ }
494
+ const resp = await fetch(signedUrl, {
495
+ method: "PUT",
496
+ headers: { "Content-Type": contentType },
497
+ body: chunk
498
+ });
499
+ if (!resp.ok) {
500
+ throw new Error(`Chunk ${i} upload failed: ${resp.status}`);
501
+ }
502
+ const eTag = resp.headers.get("ETag") || "";
503
+ uploadParts.push({ partNumber: i, eTag: JSON.parse(eTag) });
504
+ totalUploaded += chunk.length;
505
+ if (onProgress) {
506
+ const seconds = Math.round((Date.now() - startTime) / 1e3) || 1;
507
+ const speedPerSecond = formatSpeed(totalUploaded / seconds);
508
+ onProgress({
509
+ percent: `${Math.round(totalUploaded / size * 100)}%`,
510
+ loaded: totalUploaded,
511
+ total: size,
512
+ speedPerSecond,
513
+ percentRate: totalUploaded / size
514
+ });
515
+ }
516
+ }
517
+ const complete = await completeMultipartUpload({
518
+ token,
519
+ baseUrl,
520
+ originFileName: fileName,
521
+ md5,
522
+ UploadParts: uploadParts
523
+ });
524
+ logger.info(`[uploadMultipartFile] Upload complete: ${complete.ID}`);
525
+ return { fileID: complete.ID, path: complete.path, size };
526
+ }
527
+ async function sendMediaMessage(token, sessionInfo, options, baseUrl) {
528
+ const { buffer, fileName, contentType, content, onProgress } = options;
529
+ const { fileID, path } = await uploadFile(token, buffer, { fileName, contentType, onProgress }, baseUrl);
530
+ logger.info(`[sendMediaMessage] fileID=${fileID}, path=${path}`);
531
+ return sendMessage({
532
+ token,
533
+ baseUrl,
534
+ sessionInfo,
535
+ msgContent: {
536
+ content: content || "",
537
+ extraInfo: {
538
+ attechmentInfo: {
539
+ fileID: String(fileID),
540
+ fileName,
541
+ filePath: path,
542
+ fileSize: buffer.length,
543
+ mimeType: contentType
544
+ }
545
+ }
546
+ }
547
+ });
548
+ }
549
+
223
550
  // src/api/index.ts
224
551
  async function getUpdates(params) {
225
552
  const { token, baseUrl, timeout = API.DEFAULT_TIMEOUT, offset, limit = API.DEFAULT_LIMIT } = params;
@@ -236,11 +563,27 @@ async function getUpdates(params) {
236
563
  timeout: (timeout || 0) * 1e3 + HTTP.POLLING_TIMEOUT_BUFFER
237
564
  });
238
565
  }
566
+ async function getUpdatesV2(params) {
567
+ const { token, baseUrl, timeout = API.DEFAULT_TIMEOUT, limit = API.DEFAULT_LIMIT } = params;
568
+ return request({
569
+ token,
570
+ baseUrl,
571
+ method: "POST",
572
+ path: "getUpdatesV2",
573
+ body: {
574
+ timeout,
575
+ limit
576
+ },
577
+ timeout: (timeout || 0) * 1e3 + HTTP.POLLING_TIMEOUT_BUFFER
578
+ });
579
+ }
239
580
  async function sendMessage(params) {
240
581
  const { token, baseUrl, sessionInfo, msgContent } = params;
241
- if (!msgContent.content || msgContent.content.trim() === "") {
582
+ const hasContent = msgContent.content && msgContent.content.trim() !== "";
583
+ const hasAttachment = msgContent.extraInfo?.attechmentInfo || msgContent.extraInfo?.attechmentInfos && msgContent.extraInfo.attechmentInfos.length > 0;
584
+ if (!hasContent && !hasAttachment) {
242
585
  const { ValidationError: ValidationError2 } = await Promise.resolve().then(() => (init_error(), error_exports));
243
- throw new ValidationError2("Message text cannot be empty", "content", msgContent.content);
586
+ throw new ValidationError2("Message content or attachment is required", "msgContent", msgContent);
244
587
  }
245
588
  return request({
246
589
  token,
@@ -261,6 +604,7 @@ var MeetBot = class {
261
604
  baseUrl;
262
605
  pollingLimit;
263
606
  longPollingTimeout;
607
+ useV2;
264
608
  eventHandlers = /* @__PURE__ */ new Map();
265
609
  polling = false;
266
610
  offset = 0;
@@ -279,6 +623,7 @@ var MeetBot = class {
279
623
  this.baseUrl = config.baseUrl || DEFAULT_BASE_URL;
280
624
  this.pollingLimit = config.pollingLimit ?? POLLING.DEFAULT_LIMIT;
281
625
  this.longPollingTimeout = config.longPollingTimeout ?? POLLING.DEFAULT_TIMEOUT;
626
+ this.useV2 = config.useV2 ?? false;
282
627
  logger.setLevel(config.logLevel ?? "silent");
283
628
  }
284
629
  on(event, handler) {
@@ -318,7 +663,7 @@ var MeetBot = class {
318
663
  this.abortController = new AbortController();
319
664
  const limit = options?.limit ?? this.pollingLimit;
320
665
  const timeout = options?.timeout ?? this.longPollingTimeout;
321
- logger.info(`\u5F00\u59CB\u957F\u8F6E\u8BE2\u6D88\u606F... (\u6761\u6570: ${limit}, \u8D85\u65F6: ${timeout}s)`);
666
+ logger.info(`\u5F00\u59CB\u957F\u8F6E\u8BE2\u6D88\u606F... (\u6761\u6570: ${limit}, \u8D85\u65F6: ${timeout}s, V2: ${this.useV2})`);
322
667
  this.emit("polling_start", void 0);
323
668
  const retryDelay = options?.retryDelay ?? POLLING.DEFAULT_RETRY_DELAY;
324
669
  const maxRetries = options?.maxRetries ?? POLLING.DEFAULT_MAX_RETRIES;
@@ -326,20 +671,35 @@ var MeetBot = class {
326
671
  let retryCount = 0;
327
672
  while (this.polling) {
328
673
  try {
329
- const updates = await getUpdates({
330
- token: this.token,
331
- baseUrl: this.baseUrl,
332
- timeout,
333
- offset: this.offset,
334
- limit
335
- });
336
- retryCount = 0;
337
- for (const update of updates) {
338
- if (update.message) {
339
- this.emit("message", update.message);
340
- this.offset = (update.message.seqId || 0) + 1;
674
+ if (this.useV2) {
675
+ const result = await getUpdatesV2({
676
+ token: this.token,
677
+ baseUrl: this.baseUrl,
678
+ timeout,
679
+ limit
680
+ });
681
+ retryCount = 0;
682
+ for (const msgUpdate of result.msgs) {
683
+ this.emit("message", { message: msgUpdate.message, quoteMsgMap: result.quoteMsgMap });
684
+ this.offset = (msgUpdate.message.seqId || 0) + 1;
341
685
  onOffsetUpdate?.(this.offset);
342
686
  }
687
+ } else {
688
+ const updates = await getUpdates({
689
+ token: this.token,
690
+ baseUrl: this.baseUrl,
691
+ timeout,
692
+ offset: this.offset,
693
+ limit
694
+ });
695
+ retryCount = 0;
696
+ for (const update of updates) {
697
+ if (update.message) {
698
+ this.emit("message", { message: update.message, quoteMsgMap: {} });
699
+ this.offset = (update.message.seqId || 0) + 1;
700
+ onOffsetUpdate?.(this.offset);
701
+ }
702
+ }
343
703
  }
344
704
  await this.sleep(POLLING.SUCCESS_DELAY);
345
705
  } catch (error) {
@@ -370,6 +730,13 @@ var MeetBot = class {
370
730
  ...options
371
731
  });
372
732
  }
733
+ async getUpdatesV2(options) {
734
+ return getUpdatesV2({
735
+ token: this.token,
736
+ baseUrl: this.baseUrl,
737
+ ...options
738
+ });
739
+ }
373
740
  async sendMessage(sessionInfo, msgContent) {
374
741
  return sendMessage({
375
742
  token: this.token,
@@ -378,6 +745,66 @@ var MeetBot = class {
378
745
  msgContent
379
746
  });
380
747
  }
748
+ /**
749
+ * 获取单文件上传签名地址
750
+ */
751
+ async getUploadURL(params) {
752
+ return getUploadURL({
753
+ token: this.token,
754
+ baseUrl: this.baseUrl,
755
+ ...params
756
+ });
757
+ }
758
+ /**
759
+ * 获取分片上传签名地址
760
+ */
761
+ async getMultiPartUploadURL(params) {
762
+ return getMultiPartUploadURL({
763
+ token: this.token,
764
+ baseUrl: this.baseUrl,
765
+ ...params
766
+ });
767
+ }
768
+ /**
769
+ * 完成分片上传
770
+ */
771
+ async completeMultipartUpload(params) {
772
+ return completeMultipartUpload({
773
+ token: this.token,
774
+ baseUrl: this.baseUrl,
775
+ ...params
776
+ });
777
+ }
778
+ /**
779
+ * 获取文件下载地址
780
+ */
781
+ async getAccessURL(params) {
782
+ const apiParams = "sessionInfo" in params ? {
783
+ firstId: params.sessionInfo.firstID,
784
+ secondId: params.sessionInfo.secondID,
785
+ sessionType: params.sessionInfo.sessionType,
786
+ seqId: params.seqId,
787
+ fileId: params.fileId,
788
+ printResult: params.printResult
789
+ } : params;
790
+ return getAccessURL({
791
+ token: this.token,
792
+ baseUrl: this.baseUrl,
793
+ ...apiParams
794
+ });
795
+ }
796
+ /**
797
+ * 上传文件(自动选择单文件或分片上传)
798
+ */
799
+ async uploadFile(buffer, options) {
800
+ return uploadFile(this.token, buffer, options, this.baseUrl);
801
+ }
802
+ /**
803
+ * 发送媒体消息(上传并发送)
804
+ */
805
+ async sendMedia(sessionInfo, options) {
806
+ return sendMediaMessage(this.token, sessionInfo, options, this.baseUrl);
807
+ }
381
808
  sleep(ms) {
382
809
  return new Promise((resolve) => setTimeout(resolve, ms));
383
810
  }
@@ -387,11 +814,25 @@ var MeetBot = class {
387
814
  init_error();
388
815
 
389
816
  exports.API = API;
817
+ exports.CHUNK_RULES = CHUNK_RULES;
390
818
  exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
391
819
  exports.HTTP = HTTP;
392
820
  exports.MeetBot = MeetBot;
393
821
  exports.POLLING = POLLING;
822
+ exports.SESSION_TYPE = SESSION_TYPE;
823
+ exports.UPLOAD = UPLOAD;
824
+ exports.completeMultipartUpload = completeMultipartUpload;
825
+ exports.computeMD5 = computeMD5;
826
+ exports.getAccessURL = getAccessURL;
827
+ exports.getChunkNum = getChunkNum;
828
+ exports.getConvID = getConvID;
829
+ exports.getMultiPartUploadURL = getMultiPartUploadURL;
830
+ exports.getQuoteMsgKey = getQuoteMsgKey;
394
831
  exports.getUpdates = getUpdates;
832
+ exports.getUpdatesV2 = getUpdatesV2;
833
+ exports.getUploadURL = getUploadURL;
834
+ exports.sendMediaMessage = sendMediaMessage;
395
835
  exports.sendMessage = sendMessage;
836
+ exports.uploadFile = uploadFile;
396
837
  //# sourceMappingURL=index.cjs.map
397
838
  //# sourceMappingURL=index.cjs.map