@alicloud/appflow-chat 0.0.3 → 0.0.4-alpha.10

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.
@@ -35,6 +35,7 @@ export interface ModelInfo {
35
35
  config?: {
36
36
  image?: boolean;
37
37
  file?: boolean;
38
+ audio?: boolean;
38
39
  webSearch?: boolean;
39
40
  fileConfig?: string;
40
41
  };
@@ -52,10 +53,18 @@ export interface ModelCapabilities {
52
53
  };
53
54
  }
54
55
 
56
+ /** 上传结果 */
57
+ export interface UploadResult {
58
+ /** 文件下载URL */
59
+ downloadUrl: string;
60
+ /** 文件ID(仅文件类型有,图片无) */
61
+ fileId?: string;
62
+ }
63
+
55
64
  export interface ChatMessage {
56
65
  text?: string;
57
66
  images?: string[];
58
- files?: string[];
67
+ files?: Array<{ url: string; name: string; fileId?: string }>;
59
68
  audio?: string;
60
69
  modelId?: string;
61
70
  webSearch?: boolean;
@@ -80,6 +89,7 @@ export interface HistoryMessage {
80
89
  sessionId?: string;
81
90
  images?: string[];
82
91
  files?: { name: string; url: string }[];
92
+ audio?: string;
83
93
  }
84
94
 
85
95
  export interface ChatSession {
@@ -269,16 +279,18 @@ class ChatService {
269
279
  this.sendMessage(message, callbacks);
270
280
 
271
281
  // 返回链式调用对象
272
- return {
282
+ const stream: ChatStream = {
273
283
  onMessage: (callback) => {
274
284
  callbacks.onMessage = callback;
275
- return this as unknown as ChatStream;
285
+ return stream;
276
286
  },
277
287
  onError: (callback) => {
278
288
  callbacks.onError = callback;
279
- return this as unknown as ChatStream;
289
+ return stream;
280
290
  }
281
291
  };
292
+
293
+ return stream;
282
294
  }
283
295
 
284
296
  /**
@@ -326,8 +338,12 @@ class ChatService {
326
338
  richText.push({ type: 'image', mediaUrl: url });
327
339
  });
328
340
 
329
- message.files?.forEach(url => {
330
- richText.push({ type: 'file', mediaUrl: url });
341
+ message.files?.forEach(fileItem => {
342
+ const entry: any = { type: 'file', mediaUrl: fileItem.url };
343
+ if (fileItem.fileId) {
344
+ entry.mediaId = fileItem.fileId;
345
+ }
346
+ richText.push(entry);
331
347
  });
332
348
 
333
349
  requestBody.richText = richText;
@@ -449,105 +465,134 @@ class ChatService {
449
465
  }
450
466
 
451
467
  /**
452
- * 上传文件
468
+ * 发送上传事件(通用方法)
469
+ * 封装向服务端发送上传相关事件的SSE请求逻辑,支持 uploadToken 和 uploadFile 两种事件类型
470
+ * @param eventType 事件类型:'uploadToken' 获取预签名URL | 'uploadFile' 获取文件ID
471
+ * @param fileName 文件名
472
+ * @param extraData 额外数据(如 uploadFile 时需要传 content: downloadUrl)
453
473
  */
454
- async upload(file: File): Promise<string> {
455
- if (!this.isInitialized) {
456
- throw new Error('请先调用 setup() 初始化SDK');
457
- }
458
-
474
+ private async sendUploadEvent(
475
+ eventType: 'uploadToken' | 'uploadFile',
476
+ fileName: string,
477
+ extraData?: Record<string, any>
478
+ ): Promise<any> {
459
479
  const domain = this.setupConfig?.domain || '';
460
480
  const integrateId = this.config?.integrateId || '';
481
+ const { token, ticket } = await this.getRequestToken();
482
+
483
+ // uploadToken 事件:content 中包含 fileName
484
+ // uploadFile 事件:content 中包含 fileUrl(extraData 传入),不需要 fileName
485
+ const eventContent: any = eventType === 'uploadToken'
486
+ ? { fileName, ...extraData }
487
+ : { ...extraData };
488
+
489
+ const requestBody: any = {
490
+ messageType: 'event',
491
+ event: {
492
+ eventType,
493
+ content: JSON.stringify(eventContent)
494
+ }
495
+ };
461
496
 
462
- try {
463
- const { token, ticket } = await this.getRequestToken();
464
-
465
- // 获取上传预签名URL
466
- const uploadTokenResponse = await fetch(`${domain}/webhook/chatbot/chat/${integrateId}`, {
467
- method: 'POST',
468
- headers: {
469
- 'Content-Type': 'application/json',
470
- 'X-Request-Token': token,
471
- 'X-Account-Session-Ticket': ticket,
472
- },
473
- body: JSON.stringify({
474
- messageType: 'event',
475
- event: {
476
- eventType: 'uploadToken',
477
- content: JSON.stringify({ fileName: file.name })
478
- }
479
- })
480
- });
481
-
482
- // 解析SSE响应获取上传URL
483
- const reader = uploadTokenResponse.body?.getReader();
484
- if (!reader) throw new Error('无法读取响应');
485
-
486
- let uploadInfo: any = null;
487
- const decoder = new TextDecoder();
488
- let buffer = '';
489
-
490
- while (true) {
491
- const { done, value } = await reader.read();
492
- if (done) break;
493
-
494
- buffer += decoder.decode(value, { stream: true });
495
- const lines = buffer.split('\n');
497
+ // 携带 sessionId 和 chatbotModelId
498
+ if (this.sessionId) {
499
+ requestBody.sessionId = this.sessionId;
500
+ }
501
+ if (this.config?.models?.length) {
502
+ requestBody.chatbotModelId = this.config.models[0]?.id || '';
503
+ }
496
504
 
497
- // 保留最后一行(可能不完整)
498
- buffer = lines.pop() || '';
505
+ const response = await fetch(`${domain}/webhook/chatbot/chat/${integrateId}`, {
506
+ method: 'POST',
507
+ headers: {
508
+ 'Content-Type': 'application/json',
509
+ 'X-Request-Token': token,
510
+ 'X-Account-Session-Ticket': ticket,
511
+ },
512
+ body: JSON.stringify(requestBody)
513
+ });
499
514
 
500
- for (const line of lines) {
501
- const trimmedLine = line.trim();
502
- if (trimmedLine.startsWith('data:')) {
503
- try {
504
- const jsonStr = trimmedLine.slice(5).trim();
505
- if (jsonStr) {
506
- const data = JSON.parse(jsonStr);
507
- if (data.content) {
508
- // content 可能是字符串或对象
509
- const contentData = typeof data.content === 'string'
510
- ? JSON.parse(data.content)
511
- : data.content;
512
- if (contentData.uploadUrl) {
513
- uploadInfo = contentData;
514
- }
515
- }
515
+ // 解析SSE响应
516
+ const reader = response.body?.getReader();
517
+ if (!reader) throw new Error('无法读取响应');
518
+
519
+ let result: any = null;
520
+ const decoder = new TextDecoder();
521
+ let buffer = '';
522
+
523
+ while (true) {
524
+ const { done, value } = await reader.read();
525
+ if (done) break;
526
+
527
+ buffer += decoder.decode(value, { stream: true });
528
+ const lines = buffer.split('\n');
529
+
530
+ // 保留最后一行(可能不完整)
531
+ buffer = lines.pop() || '';
532
+
533
+ for (const line of lines) {
534
+ const trimmedLine = line.trim();
535
+ if (trimmedLine.startsWith('data:')) {
536
+ try {
537
+ const jsonStr = trimmedLine.slice(5).trim();
538
+ if (jsonStr) {
539
+ const data = JSON.parse(jsonStr);
540
+ if (data.content) {
541
+ // content 可能是字符串或对象
542
+ const contentData = typeof data.content === 'string'
543
+ ? JSON.parse(data.content)
544
+ : data.content;
545
+ result = contentData;
516
546
  }
517
- } catch (e) {
518
- // 忽略解析错误,继续处理下一行
519
- console.debug('解析SSE行失败:', line, e);
520
547
  }
548
+ } catch (e) {
549
+ console.debug(`解析SSE行失败(${eventType}):`, line, e);
521
550
  }
522
551
  }
523
552
  }
553
+ }
524
554
 
525
- // 处理buffer中剩余的数据
526
- if (buffer.trim().startsWith('data:')) {
527
- try {
528
- const jsonStr = buffer.trim().slice(5).trim();
529
- if (jsonStr) {
530
- const data = JSON.parse(jsonStr);
531
- if (data.content) {
532
- const contentData = typeof data.content === 'string'
533
- ? JSON.parse(data.content)
534
- : data.content;
535
- if (contentData.uploadUrl) {
536
- uploadInfo = contentData;
537
- }
538
- }
555
+ // 处理buffer中剩余的数据
556
+ if (buffer.trim().startsWith('data:')) {
557
+ try {
558
+ const jsonStr = buffer.trim().slice(5).trim();
559
+ if (jsonStr) {
560
+ const data = JSON.parse(jsonStr);
561
+ if (data.content) {
562
+ const contentData = typeof data.content === 'string'
563
+ ? JSON.parse(data.content)
564
+ : data.content;
565
+ result = contentData;
539
566
  }
540
- } catch (e) {
541
- console.debug('解析SSE剩余数据失败:', buffer, e);
542
567
  }
568
+ } catch (e) {
569
+ console.debug(`解析SSE剩余数据失败(${eventType}):`, buffer, e);
543
570
  }
571
+ }
572
+
573
+ return result;
574
+ }
575
+
576
+ /**
577
+ * 上传文件
578
+ * 非图片文件会额外获取 fileId(用于服务端文件关联)
579
+ * @returns 上传结果,包含 downloadUrl 和可选的 fileId
580
+ */
581
+ async upload(file: File): Promise<UploadResult> {
582
+ if (!this.isInitialized) {
583
+ throw new Error('请先调用 setup() 初始化SDK');
584
+ }
585
+
586
+ try {
587
+ // 1. 获取上传预签名URL
588
+ const uploadInfo = await this.sendUploadEvent('uploadToken', file.name);
544
589
 
545
590
  if (!uploadInfo?.uploadUrl) {
546
591
  console.error('获取上传URL失败,uploadInfo:', uploadInfo);
547
592
  throw new Error('获取上传URL失败');
548
593
  }
549
594
 
550
- // 上传文件
595
+ // 2. 上传文件到OSS
551
596
  const blob = new Blob([file], { type: file.type || 'application/octet-stream' });
552
597
  await fetchUploadApi(blob, uploadInfo.uploadUrl);
553
598
 
@@ -557,7 +602,21 @@ class ChatService {
557
602
  throw new Error('获取下载URL失败');
558
603
  }
559
604
 
560
- return uploadInfo.downloadUrl;
605
+ // 3. 非图片文件,额外获取 fileId
606
+ let fileId: string | undefined;
607
+ const isImage = file.type.startsWith('image/');
608
+ if (!isImage) {
609
+ try {
610
+ const fileIdResult = await this.sendUploadEvent('uploadFile', file.name, {
611
+ fileUrl: uploadInfo.downloadUrl,
612
+ });
613
+ fileId = fileIdResult?.fileId;
614
+ } catch (e) {
615
+ console.warn('获取fileId失败,将继续使用downloadUrl:', e);
616
+ }
617
+ }
618
+
619
+ return { downloadUrl: uploadInfo.downloadUrl, fileId };
561
620
  } catch (error) {
562
621
  console.error('上传文件失败:', error);
563
622
  throw error;
@@ -692,6 +751,23 @@ class ChatService {
692
751
  // 处理消息列表
693
752
  if (Array.isArray(data)) {
694
753
  for (const item of data) {
754
+ // 注意:接口返回数据是倒序的(最新在前),最终会 reverse()
755
+ // 因此这里先推入 AI 回复,再推入用户消息,reverse() 后顺序变为 user → assistant
756
+
757
+ // AI回复 - 在 assistant 数组中
758
+ if (item.assistant && Array.isArray(item.assistant) && item.assistant.length > 0) {
759
+ // 取第一个assistant回复
760
+ const assistantMsg = item.assistant[0];
761
+ messages.push({
762
+ id: assistantMsg.messageId || `assistant_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
763
+ role: 'assistant',
764
+ content: assistantMsg.content || '',
765
+ messageType: assistantMsg.messageType || 'text',
766
+ gmtCreate: item.gmtCreate,
767
+ sessionId: assistantMsg.sessionId || item.sessionId,
768
+ });
769
+ }
770
+
695
771
  // 用户消息 - 在 message 字段中
696
772
  if (item.message !== undefined && item.message !== null) {
697
773
  const messageType = item.messageType || 'text';
@@ -726,35 +802,26 @@ class ChatService {
726
802
  content = item.message;
727
803
  }
728
804
 
805
+ // 语音消息:messageType === 'audio' 时,message 是音频文件 URL
806
+ const isAudioMessage = messageType === 'audio';
807
+
729
808
  messages.push({
730
809
  id: item.id || `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
731
810
  role: 'user',
732
- content,
811
+ content: isAudioMessage ? '' : content,
733
812
  messageType,
734
813
  gmtCreate: item.gmtCreate,
735
814
  sessionId: item.sessionId,
736
815
  images,
737
816
  files,
738
- });
739
- }
740
-
741
- // AI回复 - 在 assistant 数组中
742
- if (item.assistant && Array.isArray(item.assistant) && item.assistant.length > 0) {
743
- // 取第一个assistant回复
744
- const assistantMsg = item.assistant[0];
745
- messages.push({
746
- id: assistantMsg.messageId || `assistant_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
747
- role: 'assistant',
748
- content: assistantMsg.content || '',
749
- messageType: assistantMsg.messageType || 'text',
750
- gmtCreate: item.gmtCreate,
751
- sessionId: assistantMsg.sessionId || item.sessionId,
817
+ audio: isAudioMessage ? content : undefined,
752
818
  });
753
819
  }
754
820
  }
755
821
  }
756
822
 
757
- return messages;
823
+ // 接口返回的数据是倒序的(最新的在前),需要反转为正序(最早的在前)
824
+ return messages.reverse();
758
825
  }
759
826
 
760
827
  return [];