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