@alicloud/appflow-chat 0.0.4-alpha.8 → 0.0.4-alpha.9
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/appflow-chat.cjs.js +41 -41
- package/dist/appflow-chat.esm.js +3361 -3342
- package/dist/types/index.d.ts +33 -5
- package/package.json +1 -1
- package/src/App.tsx +6 -3
- package/src/components/ChatSender.tsx +11 -9
- package/src/index.ts +1 -0
- package/src/services/ChatService.ts +123 -82
package/dist/types/index.d.ts
CHANGED
|
@@ -67,6 +67,8 @@ export declare interface ChatAttachment {
|
|
|
67
67
|
thumbUrl?: string;
|
|
68
68
|
/** 原始文件对象 */
|
|
69
69
|
originFile?: File;
|
|
70
|
+
/** 文件ID(仅文件类型有) */
|
|
71
|
+
fileId?: string;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
export declare interface ChatConfig {
|
|
@@ -86,7 +88,11 @@ export declare interface ChatConfig {
|
|
|
86
88
|
export declare interface ChatMessage {
|
|
87
89
|
text?: string;
|
|
88
90
|
images?: string[];
|
|
89
|
-
files?:
|
|
91
|
+
files?: Array<{
|
|
92
|
+
url: string;
|
|
93
|
+
name: string;
|
|
94
|
+
fileId?: string;
|
|
95
|
+
}>;
|
|
90
96
|
audio?: string;
|
|
91
97
|
modelId?: string;
|
|
92
98
|
webSearch?: boolean;
|
|
@@ -141,8 +147,11 @@ export declare interface ChatSenderProps {
|
|
|
141
147
|
onSubmit?: (data: ChatSenderSubmitData) => void;
|
|
142
148
|
/** 取消当前请求 */
|
|
143
149
|
onCancel?: () => void;
|
|
144
|
-
/** 文件上传方法,返回下载URL */
|
|
145
|
-
onUpload?: (file: File) => Promise<
|
|
150
|
+
/** 文件上传方法,返回下载URL和可选的fileId */
|
|
151
|
+
onUpload?: (file: File) => Promise<{
|
|
152
|
+
downloadUrl: string;
|
|
153
|
+
fileId?: string;
|
|
154
|
+
}>;
|
|
146
155
|
/** 自定义类名 */
|
|
147
156
|
className?: string;
|
|
148
157
|
/** 自定义样式 */
|
|
@@ -155,10 +164,11 @@ export declare interface ChatSenderSubmitData {
|
|
|
155
164
|
text: string;
|
|
156
165
|
/** 图片URL列表 */
|
|
157
166
|
images: string[];
|
|
158
|
-
/**
|
|
167
|
+
/** 文件列表(包含文件名、URL和可选的fileId) */
|
|
159
168
|
files: {
|
|
160
169
|
name: string;
|
|
161
170
|
url: string;
|
|
171
|
+
fileId?: string;
|
|
162
172
|
}[];
|
|
163
173
|
/** 语音文件URL(录音上传后的下载地址) */
|
|
164
174
|
audio?: string;
|
|
@@ -198,10 +208,20 @@ export declare class ChatService {
|
|
|
198
208
|
* 发送消息的内部实现
|
|
199
209
|
*/
|
|
200
210
|
private sendMessage;
|
|
211
|
+
/**
|
|
212
|
+
* 发送上传事件(通用方法)
|
|
213
|
+
* 封装向服务端发送上传相关事件的SSE请求逻辑,支持 uploadToken 和 uploadFile 两种事件类型
|
|
214
|
+
* @param eventType 事件类型:'uploadToken' 获取预签名URL | 'uploadFile' 获取文件ID
|
|
215
|
+
* @param fileName 文件名
|
|
216
|
+
* @param extraData 额外数据(如 uploadFile 时需要传 content: downloadUrl)
|
|
217
|
+
*/
|
|
218
|
+
private sendUploadEvent;
|
|
201
219
|
/**
|
|
202
220
|
* 上传文件
|
|
221
|
+
* 非图片文件会额外获取 fileId(用于服务端文件关联)
|
|
222
|
+
* @returns 上传结果,包含 downloadUrl 和可选的 fileId
|
|
203
223
|
*/
|
|
204
|
-
upload(file: File): Promise<
|
|
224
|
+
upload(file: File): Promise<UploadResult>;
|
|
205
225
|
/**
|
|
206
226
|
* 清除会话
|
|
207
227
|
*/
|
|
@@ -871,6 +891,14 @@ declare interface UploadRequestParams {
|
|
|
871
891
|
content?: string;
|
|
872
892
|
}
|
|
873
893
|
|
|
894
|
+
/** 上传结果 */
|
|
895
|
+
export declare interface UploadResult {
|
|
896
|
+
/** 文件下载URL */
|
|
897
|
+
downloadUrl: string;
|
|
898
|
+
/** 文件ID(仅文件类型有,图片无) */
|
|
899
|
+
fileId?: string;
|
|
900
|
+
}
|
|
901
|
+
|
|
874
902
|
/**
|
|
875
903
|
* 上传发送方法类型
|
|
876
904
|
* 用于发送上传相关的事件请求
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -13,14 +13,17 @@ const mockModels: ModelInfo[] = [
|
|
|
13
13
|
];
|
|
14
14
|
|
|
15
15
|
// 模拟上传
|
|
16
|
-
const mockUpload = async (file: File): Promise<string> => {
|
|
16
|
+
const mockUpload = async (file: File): Promise<{ downloadUrl: string; fileId?: string }> => {
|
|
17
17
|
return new Promise((resolve) => {
|
|
18
18
|
setTimeout(() => {
|
|
19
19
|
// 音频文件返回 blob URL,以便本地测试播放
|
|
20
20
|
if (file.type.startsWith('audio/')) {
|
|
21
|
-
resolve(URL.createObjectURL(file));
|
|
21
|
+
resolve({ downloadUrl: URL.createObjectURL(file) });
|
|
22
|
+
} else if (file.type.startsWith('image/')) {
|
|
23
|
+
resolve({ downloadUrl: `https://example.com/files/${file.name}` });
|
|
22
24
|
} else {
|
|
23
|
-
|
|
25
|
+
// 非图片文件模拟返回 fileId
|
|
26
|
+
resolve({ downloadUrl: `https://example.com/files/${file.name}`, fileId: `mock_fid_${Date.now()}` });
|
|
24
27
|
}
|
|
25
28
|
}, 1500);
|
|
26
29
|
});
|
|
@@ -49,6 +49,8 @@ export interface ChatAttachment {
|
|
|
49
49
|
thumbUrl?: string;
|
|
50
50
|
/** 原始文件对象 */
|
|
51
51
|
originFile?: File;
|
|
52
|
+
/** 文件ID(仅文件类型有) */
|
|
53
|
+
fileId?: string;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
/** 提交时的消息数据 */
|
|
@@ -57,8 +59,8 @@ export interface ChatSenderSubmitData {
|
|
|
57
59
|
text: string;
|
|
58
60
|
/** 图片URL列表 */
|
|
59
61
|
images: string[];
|
|
60
|
-
/**
|
|
61
|
-
files: { name: string; url: string }[];
|
|
62
|
+
/** 文件列表(包含文件名、URL和可选的fileId) */
|
|
63
|
+
files: { name: string; url: string; fileId?: string }[];
|
|
62
64
|
/** 语音文件URL(录音上传后的下载地址) */
|
|
63
65
|
audio?: string;
|
|
64
66
|
/** 选中的模型ID */
|
|
@@ -102,8 +104,8 @@ export interface ChatSenderProps {
|
|
|
102
104
|
onSubmit?: (data: ChatSenderSubmitData) => void;
|
|
103
105
|
/** 取消当前请求 */
|
|
104
106
|
onCancel?: () => void;
|
|
105
|
-
/** 文件上传方法,返回下载URL */
|
|
106
|
-
onUpload?: (file: File) => Promise<string>;
|
|
107
|
+
/** 文件上传方法,返回下载URL和可选的fileId */
|
|
108
|
+
onUpload?: (file: File) => Promise<{ downloadUrl: string; fileId?: string }>;
|
|
107
109
|
|
|
108
110
|
// ==================== 样式 ====================
|
|
109
111
|
|
|
@@ -330,9 +332,9 @@ export const ChatSender: React.FC<ChatSenderProps> = ({
|
|
|
330
332
|
setHeaderOpen(true);
|
|
331
333
|
|
|
332
334
|
try {
|
|
333
|
-
const
|
|
335
|
+
const result = await onUpload(file);
|
|
334
336
|
setAttachments(prev =>
|
|
335
|
-
prev.map(a => a.uid === uid ? { ...a, status: 'done' as const, url: downloadUrl } : a)
|
|
337
|
+
prev.map(a => a.uid === uid ? { ...a, status: 'done' as const, url: result.downloadUrl, fileId: result.fileId } : a)
|
|
336
338
|
);
|
|
337
339
|
} catch {
|
|
338
340
|
setAttachments(prev =>
|
|
@@ -353,7 +355,7 @@ export const ChatSender: React.FC<ChatSenderProps> = ({
|
|
|
353
355
|
|
|
354
356
|
const files = attachments
|
|
355
357
|
.filter(a => a.type === 'file' && a.status === 'done' && a.url)
|
|
356
|
-
.map(a => ({ name: a.name, url: a.url
|
|
358
|
+
.map(a => ({ name: a.name, url: a.url!, fileId: a.fileId }));
|
|
357
359
|
|
|
358
360
|
onSubmit?.({
|
|
359
361
|
text: text.trim(),
|
|
@@ -518,14 +520,14 @@ export const ChatSender: React.FC<ChatSenderProps> = ({
|
|
|
518
520
|
|
|
519
521
|
try {
|
|
520
522
|
// 上传音频文件
|
|
521
|
-
const
|
|
523
|
+
const result = await onUpload(audioFile);
|
|
522
524
|
|
|
523
525
|
// 发送语音消息(audio 优先级最高,服务端会忽略 text/images/files)
|
|
524
526
|
onSubmit?.({
|
|
525
527
|
text: '',
|
|
526
528
|
images: [],
|
|
527
529
|
files: [],
|
|
528
|
-
audio:
|
|
530
|
+
audio: result.downloadUrl,
|
|
529
531
|
modelId: currentModelId,
|
|
530
532
|
webSearch: false,
|
|
531
533
|
});
|
package/src/index.ts
CHANGED
|
@@ -53,10 +53,18 @@ export interface ModelCapabilities {
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/** 上传结果 */
|
|
57
|
+
export interface UploadResult {
|
|
58
|
+
/** 文件下载URL */
|
|
59
|
+
downloadUrl: string;
|
|
60
|
+
/** 文件ID(仅文件类型有,图片无) */
|
|
61
|
+
fileId?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
56
64
|
export interface ChatMessage {
|
|
57
65
|
text?: string;
|
|
58
66
|
images?: string[];
|
|
59
|
-
files?: string
|
|
67
|
+
files?: Array<{ url: string; name: string; fileId?: string }>;
|
|
60
68
|
audio?: string;
|
|
61
69
|
modelId?: string;
|
|
62
70
|
webSearch?: boolean;
|
|
@@ -330,8 +338,12 @@ class ChatService {
|
|
|
330
338
|
richText.push({ type: 'image', mediaUrl: url });
|
|
331
339
|
});
|
|
332
340
|
|
|
333
|
-
message.files?.forEach(
|
|
334
|
-
|
|
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);
|
|
335
347
|
});
|
|
336
348
|
|
|
337
349
|
requestBody.richText = richText;
|
|
@@ -453,105 +465,120 @@ class ChatService {
|
|
|
453
465
|
}
|
|
454
466
|
|
|
455
467
|
/**
|
|
456
|
-
*
|
|
468
|
+
* 发送上传事件(通用方法)
|
|
469
|
+
* 封装向服务端发送上传相关事件的SSE请求逻辑,支持 uploadToken 和 uploadFile 两种事件类型
|
|
470
|
+
* @param eventType 事件类型:'uploadToken' 获取预签名URL | 'uploadFile' 获取文件ID
|
|
471
|
+
* @param fileName 文件名
|
|
472
|
+
* @param extraData 额外数据(如 uploadFile 时需要传 content: downloadUrl)
|
|
457
473
|
*/
|
|
458
|
-
async
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
474
|
+
private async sendUploadEvent(
|
|
475
|
+
eventType: 'uploadToken' | 'uploadFile',
|
|
476
|
+
fileName: string,
|
|
477
|
+
extraData?: Record<string, any>
|
|
478
|
+
): Promise<any> {
|
|
463
479
|
const domain = this.setupConfig?.domain || '';
|
|
464
480
|
const integrateId = this.config?.integrateId || '';
|
|
481
|
+
const { token, ticket } = await this.getRequestToken();
|
|
465
482
|
|
|
466
|
-
|
|
467
|
-
const { token, ticket } = await this.getRequestToken();
|
|
483
|
+
const eventContent: any = { fileName, ...extraData };
|
|
468
484
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
// 解析SSE响应获取上传URL
|
|
487
|
-
const reader = uploadTokenResponse.body?.getReader();
|
|
488
|
-
if (!reader) throw new Error('无法读取响应');
|
|
489
|
-
|
|
490
|
-
let uploadInfo: any = null;
|
|
491
|
-
const decoder = new TextDecoder();
|
|
492
|
-
let buffer = '';
|
|
493
|
-
|
|
494
|
-
while (true) {
|
|
495
|
-
const { done, value } = await reader.read();
|
|
496
|
-
if (done) break;
|
|
497
|
-
|
|
498
|
-
buffer += decoder.decode(value, { stream: true });
|
|
499
|
-
const lines = buffer.split('\n');
|
|
500
|
-
|
|
501
|
-
// 保留最后一行(可能不完整)
|
|
502
|
-
buffer = lines.pop() || '';
|
|
485
|
+
const response = await fetch(`${domain}/webhook/chatbot/chat/${integrateId}`, {
|
|
486
|
+
method: 'POST',
|
|
487
|
+
headers: {
|
|
488
|
+
'Content-Type': 'application/json',
|
|
489
|
+
'X-Request-Token': token,
|
|
490
|
+
'X-Account-Session-Ticket': ticket,
|
|
491
|
+
},
|
|
492
|
+
body: JSON.stringify({
|
|
493
|
+
messageType: 'event',
|
|
494
|
+
event: {
|
|
495
|
+
eventType,
|
|
496
|
+
content: JSON.stringify(eventContent)
|
|
497
|
+
}
|
|
498
|
+
})
|
|
499
|
+
});
|
|
503
500
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
501
|
+
// 解析SSE响应
|
|
502
|
+
const reader = response.body?.getReader();
|
|
503
|
+
if (!reader) throw new Error('无法读取响应');
|
|
504
|
+
|
|
505
|
+
let result: any = null;
|
|
506
|
+
const decoder = new TextDecoder();
|
|
507
|
+
let buffer = '';
|
|
508
|
+
|
|
509
|
+
while (true) {
|
|
510
|
+
const { done, value } = await reader.read();
|
|
511
|
+
if (done) break;
|
|
512
|
+
|
|
513
|
+
buffer += decoder.decode(value, { stream: true });
|
|
514
|
+
const lines = buffer.split('\n');
|
|
515
|
+
|
|
516
|
+
// 保留最后一行(可能不完整)
|
|
517
|
+
buffer = lines.pop() || '';
|
|
518
|
+
|
|
519
|
+
for (const line of lines) {
|
|
520
|
+
const trimmedLine = line.trim();
|
|
521
|
+
if (trimmedLine.startsWith('data:')) {
|
|
522
|
+
try {
|
|
523
|
+
const jsonStr = trimmedLine.slice(5).trim();
|
|
524
|
+
if (jsonStr) {
|
|
525
|
+
const data = JSON.parse(jsonStr);
|
|
526
|
+
if (data.content) {
|
|
527
|
+
// content 可能是字符串或对象
|
|
528
|
+
const contentData = typeof data.content === 'string'
|
|
529
|
+
? JSON.parse(data.content)
|
|
530
|
+
: data.content;
|
|
531
|
+
result = contentData;
|
|
520
532
|
}
|
|
521
|
-
} catch (e) {
|
|
522
|
-
// 忽略解析错误,继续处理下一行
|
|
523
|
-
console.debug('解析SSE行失败:', line, e);
|
|
524
533
|
}
|
|
534
|
+
} catch (e) {
|
|
535
|
+
console.debug(`解析SSE行失败(${eventType}):`, line, e);
|
|
525
536
|
}
|
|
526
537
|
}
|
|
527
538
|
}
|
|
539
|
+
}
|
|
528
540
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
uploadInfo = contentData;
|
|
541
|
-
}
|
|
542
|
-
}
|
|
541
|
+
// 处理buffer中剩余的数据
|
|
542
|
+
if (buffer.trim().startsWith('data:')) {
|
|
543
|
+
try {
|
|
544
|
+
const jsonStr = buffer.trim().slice(5).trim();
|
|
545
|
+
if (jsonStr) {
|
|
546
|
+
const data = JSON.parse(jsonStr);
|
|
547
|
+
if (data.content) {
|
|
548
|
+
const contentData = typeof data.content === 'string'
|
|
549
|
+
? JSON.parse(data.content)
|
|
550
|
+
: data.content;
|
|
551
|
+
result = contentData;
|
|
543
552
|
}
|
|
544
|
-
} catch (e) {
|
|
545
|
-
console.debug('解析SSE剩余数据失败:', buffer, e);
|
|
546
553
|
}
|
|
554
|
+
} catch (e) {
|
|
555
|
+
console.debug(`解析SSE剩余数据失败(${eventType}):`, buffer, e);
|
|
547
556
|
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return result;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* 上传文件
|
|
564
|
+
* 非图片文件会额外获取 fileId(用于服务端文件关联)
|
|
565
|
+
* @returns 上传结果,包含 downloadUrl 和可选的 fileId
|
|
566
|
+
*/
|
|
567
|
+
async upload(file: File): Promise<UploadResult> {
|
|
568
|
+
if (!this.isInitialized) {
|
|
569
|
+
throw new Error('请先调用 setup() 初始化SDK');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
try {
|
|
573
|
+
// 1. 获取上传预签名URL
|
|
574
|
+
const uploadInfo = await this.sendUploadEvent('uploadToken', file.name);
|
|
548
575
|
|
|
549
576
|
if (!uploadInfo?.uploadUrl) {
|
|
550
577
|
console.error('获取上传URL失败,uploadInfo:', uploadInfo);
|
|
551
578
|
throw new Error('获取上传URL失败');
|
|
552
579
|
}
|
|
553
580
|
|
|
554
|
-
//
|
|
581
|
+
// 2. 上传文件到OSS
|
|
555
582
|
const blob = new Blob([file], { type: file.type || 'application/octet-stream' });
|
|
556
583
|
await fetchUploadApi(blob, uploadInfo.uploadUrl);
|
|
557
584
|
|
|
@@ -561,7 +588,21 @@ class ChatService {
|
|
|
561
588
|
throw new Error('获取下载URL失败');
|
|
562
589
|
}
|
|
563
590
|
|
|
564
|
-
|
|
591
|
+
// 3. 非图片文件,额外获取 fileId
|
|
592
|
+
let fileId: string | undefined;
|
|
593
|
+
const isImage = file.type.startsWith('image/');
|
|
594
|
+
if (!isImage) {
|
|
595
|
+
try {
|
|
596
|
+
const fileIdResult = await this.sendUploadEvent('uploadFile', file.name, {
|
|
597
|
+
content: uploadInfo.downloadUrl,
|
|
598
|
+
});
|
|
599
|
+
fileId = fileIdResult?.fileId;
|
|
600
|
+
} catch (e) {
|
|
601
|
+
console.warn('获取fileId失败,将继续使用downloadUrl:', e);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return { downloadUrl: uploadInfo.downloadUrl, fileId };
|
|
565
606
|
} catch (error) {
|
|
566
607
|
console.error('上传文件失败:', error);
|
|
567
608
|
throw error;
|