@alicloud/appflow-chat 0.0.1-beta.1
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/README-ZH.md +188 -0
- package/README.md +190 -0
- package/dist/appflow-chat.cjs.js +1903 -0
- package/dist/appflow-chat.esm.js +36965 -0
- package/dist/types/index.d.ts +862 -0
- package/package.json +87 -0
- package/src/components/DocReferences.tsx +64 -0
- package/src/components/HumanVerify/CustomParamsRenderer/ArrayField.tsx +394 -0
- package/src/components/HumanVerify/CustomParamsRenderer/FieldRenderer.tsx +202 -0
- package/src/components/HumanVerify/CustomParamsRenderer/ObjectField.tsx +126 -0
- package/src/components/HumanVerify/CustomParamsRenderer/index.tsx +166 -0
- package/src/components/HumanVerify/CustomParamsRenderer/types.ts +203 -0
- package/src/components/HumanVerify/HistoryCard.tsx +156 -0
- package/src/components/HumanVerify/HumanVerify.tsx +184 -0
- package/src/components/HumanVerify/index.ts +11 -0
- package/src/components/MarkdownRenderer.tsx +195 -0
- package/src/components/MessageBubble.tsx +400 -0
- package/src/components/RichMessageBubble.tsx +283 -0
- package/src/components/WebSearchPanel.tsx +68 -0
- package/src/context/RichBubble.tsx +21 -0
- package/src/core/BubbleContent.tsx +75 -0
- package/src/core/RichBubbleContent.tsx +324 -0
- package/src/core/SourceContent.tsx +285 -0
- package/src/core/WebSearchContent.tsx +219 -0
- package/src/core/index.ts +16 -0
- package/src/hooks/usePreSignUpload.ts +36 -0
- package/src/index.ts +80 -0
- package/src/markdown/components/Chart.tsx +120 -0
- package/src/markdown/components/Error.tsx +39 -0
- package/src/markdown/components/FileDisplay.tsx +246 -0
- package/src/markdown/components/Loading.tsx +41 -0
- package/src/markdown/components/SyntaxHighlight.tsx +182 -0
- package/src/markdown/index.tsx +250 -0
- package/src/markdown/styled.ts +234 -0
- package/src/markdown/utils/dataProcessor.ts +89 -0
- package/src/services/ChatService.ts +926 -0
- package/src/utils/fetchEventSource.ts +65 -0
- package/src/utils/loadEcharts.ts +32 -0
- package/src/utils/loadPrism.ts +156 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,926 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatService - 独立的聊天服务类
|
|
3
|
+
* 用于自定义UI场景,不依赖React
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { fetchEventSource } from '@/utils/fetchEventSource';
|
|
7
|
+
import { fetchUploadApi } from '@/hooks/usePreSignUpload';
|
|
8
|
+
import Cookies from 'js-cookie';
|
|
9
|
+
|
|
10
|
+
// ==================== 类型定义 ====================
|
|
11
|
+
|
|
12
|
+
export interface SetupConfig {
|
|
13
|
+
integrateId: string;
|
|
14
|
+
domain?: string;
|
|
15
|
+
access_session_token?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ChatConfig {
|
|
19
|
+
welcome: string;
|
|
20
|
+
questions: string[];
|
|
21
|
+
models: ModelInfo[];
|
|
22
|
+
features: {
|
|
23
|
+
image: boolean;
|
|
24
|
+
file: boolean;
|
|
25
|
+
audio: boolean;
|
|
26
|
+
webSearch: boolean;
|
|
27
|
+
};
|
|
28
|
+
chatbotId: string;
|
|
29
|
+
integrateId: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ModelInfo {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
config?: {
|
|
36
|
+
image?: boolean;
|
|
37
|
+
file?: boolean;
|
|
38
|
+
webSearch?: boolean;
|
|
39
|
+
fileConfig?: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ModelCapabilities {
|
|
44
|
+
image: boolean;
|
|
45
|
+
file: boolean;
|
|
46
|
+
audio: boolean;
|
|
47
|
+
webSearch: boolean;
|
|
48
|
+
fileConfig?: {
|
|
49
|
+
supportFileTypes?: string[];
|
|
50
|
+
limit?: number;
|
|
51
|
+
description?: string;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ChatMessage {
|
|
56
|
+
text?: string;
|
|
57
|
+
images?: string[];
|
|
58
|
+
files?: string[];
|
|
59
|
+
audio?: string;
|
|
60
|
+
modelId?: string;
|
|
61
|
+
webSearch?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ChatStreamCallbacks {
|
|
65
|
+
onMessage?: (content: string, done: boolean, meta?: any) => void;
|
|
66
|
+
onError?: (error: Error) => void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ChatStream {
|
|
70
|
+
onMessage(callback: (content: string, done: boolean, meta?: any) => void): ChatStream;
|
|
71
|
+
onError(callback: (error: Error) => void): ChatStream;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface HistoryMessage {
|
|
75
|
+
id: string;
|
|
76
|
+
role: 'user' | 'assistant';
|
|
77
|
+
content: string;
|
|
78
|
+
messageType: string;
|
|
79
|
+
gmtCreate?: string;
|
|
80
|
+
sessionId?: string;
|
|
81
|
+
images?: string[];
|
|
82
|
+
files?: { name: string; url: string }[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ChatSession {
|
|
86
|
+
id: string;
|
|
87
|
+
sessionId: string;
|
|
88
|
+
title: string;
|
|
89
|
+
gmtCreate?: string;
|
|
90
|
+
gmtModified?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ==================== ChatService 类 ====================
|
|
94
|
+
|
|
95
|
+
class ChatService {
|
|
96
|
+
private config: ChatConfig | null = null;
|
|
97
|
+
private setupConfig: SetupConfig | null = null;
|
|
98
|
+
private sessionId: string = '';
|
|
99
|
+
private currentController: AbortController | null = null;
|
|
100
|
+
private isInitialized: boolean = false;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 初始化SDK并返回配置信息
|
|
104
|
+
*/
|
|
105
|
+
async setup(config: SetupConfig): Promise<ChatConfig> {
|
|
106
|
+
this.setupConfig = config;
|
|
107
|
+
|
|
108
|
+
// 设置全局配置(兼容现有逻辑)
|
|
109
|
+
if (typeof window !== 'undefined') {
|
|
110
|
+
(window as any).__APPFLOW_CHAT_SERVICE_CONFIG__ = {
|
|
111
|
+
integrateConfig: {
|
|
112
|
+
integrateId: config.integrateId,
|
|
113
|
+
domain: config.domain ? { requestDomain: config.domain } : undefined,
|
|
114
|
+
access_session_token: config.access_session_token,
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 清除旧的ticket(如果有新的access_session_token)
|
|
120
|
+
if (config.access_session_token) {
|
|
121
|
+
Cookies.remove('appflow_chat_ticket');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 获取认证票据
|
|
125
|
+
await this.initTicket();
|
|
126
|
+
|
|
127
|
+
// 获取聊天机器人配置
|
|
128
|
+
const chatbotConfig = await this.fetchConfig(config.integrateId);
|
|
129
|
+
this.config = chatbotConfig;
|
|
130
|
+
this.isInitialized = true;
|
|
131
|
+
|
|
132
|
+
return chatbotConfig;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 初始化认证票据
|
|
137
|
+
*/
|
|
138
|
+
private async initTicket(): Promise<void> {
|
|
139
|
+
const domain = this.setupConfig?.domain || '';
|
|
140
|
+
const ticketFromCookie = Cookies.get('appflow_chat_ticket');
|
|
141
|
+
|
|
142
|
+
if (!ticketFromCookie) {
|
|
143
|
+
try {
|
|
144
|
+
const response = await fetch(`${domain}/webhook/login/init/ticket`, {
|
|
145
|
+
method: 'POST',
|
|
146
|
+
headers: { 'Content-Type': 'application/json' },
|
|
147
|
+
body: JSON.stringify({
|
|
148
|
+
loginType: 'chatbot_integrate',
|
|
149
|
+
integrateId: this.setupConfig?.integrateId,
|
|
150
|
+
accessSessionToken: this.setupConfig?.access_session_token
|
|
151
|
+
})
|
|
152
|
+
});
|
|
153
|
+
const result = await response.json();
|
|
154
|
+
if (result?.data && result?.code === '200') {
|
|
155
|
+
Cookies.set('appflow_chat_ticket', result.data, { expires: 7 });
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error('初始化ticket失败:', error);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 获取聊天机器人配置
|
|
165
|
+
*/
|
|
166
|
+
private async fetchConfig(integrateId: string): Promise<ChatConfig> {
|
|
167
|
+
const domain = this.setupConfig?.domain || '';
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const { token, ticket } = await this.getRequestToken();
|
|
171
|
+
|
|
172
|
+
const response = await fetch(`${domain}/webhook/chatbot/integrate/${integrateId}`, {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers: {
|
|
175
|
+
'Content-Type': 'application/json',
|
|
176
|
+
'X-Request-Token': token,
|
|
177
|
+
'X-Account-Session-Ticket': ticket,
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const result = await response.json();
|
|
182
|
+
|
|
183
|
+
if (result?.httpStatusCode === 200 && result?.data) {
|
|
184
|
+
const data = result.data;
|
|
185
|
+
const configStr = data.integrateConfig;
|
|
186
|
+
const parsedConfig = configStr ? JSON.parse(configStr) : {};
|
|
187
|
+
|
|
188
|
+
// 解析模型列表
|
|
189
|
+
const models: ModelInfo[] = (data.models || []).map((m: any) => ({
|
|
190
|
+
id: m.chatbotModelId || m.ChatbotModelId,
|
|
191
|
+
name: m.modelName || m.ModelName || m.chatbotModelId,
|
|
192
|
+
config: {
|
|
193
|
+
image: m.config?.image || m.Config?.image || m.Config?.Image,
|
|
194
|
+
file: m.config?.file || m.Config?.file || m.Config?.File,
|
|
195
|
+
webSearch: m.config?.webSearch || m.Config?.webSearch || m.Config?.WebSearch,
|
|
196
|
+
}
|
|
197
|
+
}));
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
welcome: parsedConfig?.message?.welcome || '欢迎使用AI助手',
|
|
201
|
+
questions: parsedConfig?.config?.questions || [],
|
|
202
|
+
models,
|
|
203
|
+
features: {
|
|
204
|
+
image: parsedConfig?.message?.imageEnabled || false,
|
|
205
|
+
file: parsedConfig?.message?.fileEnabled || false,
|
|
206
|
+
audio: parsedConfig?.message?.audioEnabled || false,
|
|
207
|
+
webSearch: models.some(m => m.config?.webSearch),
|
|
208
|
+
},
|
|
209
|
+
chatbotId: data.chatbotId,
|
|
210
|
+
integrateId: integrateId,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
throw new Error('获取配置失败');
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.error('获取配置失败:', error);
|
|
217
|
+
// 返回默认配置
|
|
218
|
+
return {
|
|
219
|
+
welcome: '欢迎使用AI助手',
|
|
220
|
+
questions: [],
|
|
221
|
+
models: [],
|
|
222
|
+
features: { image: false, file: false, audio: false, webSearch: false },
|
|
223
|
+
chatbotId: '',
|
|
224
|
+
integrateId: integrateId,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 获取请求令牌
|
|
231
|
+
*/
|
|
232
|
+
private async getRequestToken(): Promise<{ token: string; ticket: string }> {
|
|
233
|
+
const domain = this.setupConfig?.domain || '';
|
|
234
|
+
const ticket = Cookies.get('appflow_chat_ticket') || '';
|
|
235
|
+
|
|
236
|
+
if (!ticket) {
|
|
237
|
+
throw new Error('未找到认证票据,请先调用setup');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const response = await fetch(`${domain}/webhook/request/token/acquireRequestToken`, {
|
|
241
|
+
method: 'POST',
|
|
242
|
+
headers: {
|
|
243
|
+
'Content-Type': 'application/json',
|
|
244
|
+
'X-Account-Session-Ticket': ticket,
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const result = await response.json();
|
|
249
|
+
const token = result?.data;
|
|
250
|
+
|
|
251
|
+
if (!token) {
|
|
252
|
+
throw new Error('获取请求令牌失败');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { token, ticket };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 发送消息(流式响应)
|
|
260
|
+
*/
|
|
261
|
+
chat(message: ChatMessage): ChatStream {
|
|
262
|
+
if (!this.isInitialized || !this.config) {
|
|
263
|
+
throw new Error('请先调用 setup() 初始化SDK');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const callbacks: ChatStreamCallbacks = {};
|
|
267
|
+
|
|
268
|
+
// 异步执行发送逻辑
|
|
269
|
+
this.sendMessage(message, callbacks);
|
|
270
|
+
|
|
271
|
+
// 返回链式调用对象
|
|
272
|
+
return {
|
|
273
|
+
onMessage: (callback) => {
|
|
274
|
+
callbacks.onMessage = callback;
|
|
275
|
+
return this as unknown as ChatStream;
|
|
276
|
+
},
|
|
277
|
+
onError: (callback) => {
|
|
278
|
+
callbacks.onError = callback;
|
|
279
|
+
return this as unknown as ChatStream;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 发送消息的内部实现
|
|
286
|
+
*/
|
|
287
|
+
private async sendMessage(message: ChatMessage, callbacks: ChatStreamCallbacks): Promise<void> {
|
|
288
|
+
const domain = this.setupConfig?.domain || '';
|
|
289
|
+
const integrateId = this.config?.integrateId || '';
|
|
290
|
+
const modelId = message.modelId || this.config?.models[0]?.id;
|
|
291
|
+
|
|
292
|
+
// 创建新的AbortController
|
|
293
|
+
this.currentController = new AbortController();
|
|
294
|
+
|
|
295
|
+
// 用于存储完整内容(需要在try-catch外部声明以便catch中访问)
|
|
296
|
+
let fullContent = '';
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const { token, ticket } = await this.getRequestToken();
|
|
300
|
+
|
|
301
|
+
// 构建请求体
|
|
302
|
+
const requestBody: any = {
|
|
303
|
+
chatbotId: this.config?.chatbotId,
|
|
304
|
+
chatbotModelId: modelId,
|
|
305
|
+
sessionId: this.sessionId,
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// 处理不同类型的消息
|
|
309
|
+
if (message.audio) {
|
|
310
|
+
// 语音消息
|
|
311
|
+
requestBody.messageType = 'audio';
|
|
312
|
+
requestBody.audio = {
|
|
313
|
+
mediaUrl: message.audio,
|
|
314
|
+
mediaType: 'wav'
|
|
315
|
+
};
|
|
316
|
+
} else if ((message.images && message.images.length > 0) || (message.files && message.files.length > 0)) {
|
|
317
|
+
// 富文本消息(包含图片或文件)
|
|
318
|
+
requestBody.messageType = 'rich';
|
|
319
|
+
const richText: any[] = [];
|
|
320
|
+
|
|
321
|
+
if (message.text) {
|
|
322
|
+
richText.push({ type: 'text', content: message.text });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
message.images?.forEach(url => {
|
|
326
|
+
richText.push({ type: 'image', mediaUrl: url });
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
message.files?.forEach(url => {
|
|
330
|
+
richText.push({ type: 'file', mediaUrl: url });
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
requestBody.richText = richText;
|
|
334
|
+
} else {
|
|
335
|
+
// 纯文本消息
|
|
336
|
+
requestBody.messageType = 'text';
|
|
337
|
+
requestBody.text = { content: message.text || '' };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 添加联网搜索配置
|
|
341
|
+
if (message.webSearch) {
|
|
342
|
+
requestBody.config = { webSearch: true };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
await fetchEventSource(
|
|
346
|
+
`${domain}/webhook/chatbot/chat/${integrateId}`,
|
|
347
|
+
{
|
|
348
|
+
method: 'POST',
|
|
349
|
+
headers: {
|
|
350
|
+
'Content-Type': 'application/json',
|
|
351
|
+
'X-Request-Token': token,
|
|
352
|
+
'X-Account-Session-Ticket': ticket,
|
|
353
|
+
},
|
|
354
|
+
body: JSON.stringify(requestBody),
|
|
355
|
+
signal: this.currentController.signal,
|
|
356
|
+
timeout: 150 * 1000,
|
|
357
|
+
onopen: async (response: Response) => {
|
|
358
|
+
if (!response.ok && response.status !== 200) {
|
|
359
|
+
const errorText = await response.text();
|
|
360
|
+
throw new Error(errorText);
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
onmessage: (event: any) => {
|
|
364
|
+
try {
|
|
365
|
+
const data = JSON.parse(event.data);
|
|
366
|
+
|
|
367
|
+
// 更新sessionId
|
|
368
|
+
if (data.sessionId) {
|
|
369
|
+
this.sessionId = data.sessionId;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 处理事件消息
|
|
373
|
+
if (data.messageType === 'event') {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
fullContent = data.content || '';
|
|
378
|
+
const isDone = data.status !== 'Running';
|
|
379
|
+
|
|
380
|
+
// 构建meta对象
|
|
381
|
+
const meta: Record<string, any> = {
|
|
382
|
+
status: data.status,
|
|
383
|
+
references: data.references,
|
|
384
|
+
sessionId: data.sessionId,
|
|
385
|
+
messageType: data.messageType,
|
|
386
|
+
messageId: data.messageId,
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// 处理 card 类型消息(humanVerify 等)
|
|
390
|
+
if (data.messageType === 'card') {
|
|
391
|
+
meta.sessionWebhook = data.sessionWebhook;
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
const cardContent = JSON.parse(data.content);
|
|
395
|
+
meta.cardType = cardContent.type;
|
|
396
|
+
meta.cardData = cardContent.data;
|
|
397
|
+
meta.cardDisplayContent = cardContent.content;
|
|
398
|
+
|
|
399
|
+
// 处理 chatbot_input 类型(humanVerify 表单)
|
|
400
|
+
if (cardContent.type === 'chatbot_input') {
|
|
401
|
+
const innerData = cardContent.data;
|
|
402
|
+
meta.verifyId = innerData?.verifyId;
|
|
403
|
+
|
|
404
|
+
// 查找 customParams(AssociationProperty === 'CustomParams')
|
|
405
|
+
if (innerData) {
|
|
406
|
+
for (const key in innerData) {
|
|
407
|
+
if (innerData[key] &&
|
|
408
|
+
typeof innerData[key] === 'object' &&
|
|
409
|
+
innerData[key].AssociationProperty === 'CustomParams') {
|
|
410
|
+
meta.customParamsKey = key;
|
|
411
|
+
meta.customParams = innerData[key];
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// 处理 update_chatbot_input 类型(审核状态更新)
|
|
419
|
+
if (cardContent.type === 'update_chatbot_input') {
|
|
420
|
+
const innerData = cardContent.data;
|
|
421
|
+
meta.approvedMessageId = innerData?.messageId;
|
|
422
|
+
meta.approvedStatus = innerData?.status;
|
|
423
|
+
}
|
|
424
|
+
} catch (parseError) {
|
|
425
|
+
console.error('解析 card content 失败:', parseError);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
callbacks.onMessage?.(fullContent, isDone, meta);
|
|
430
|
+
} catch (e) {
|
|
431
|
+
console.error('解析消息失败:', e);
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
onclose: () => {
|
|
435
|
+
callbacks.onMessage?.(fullContent, true);
|
|
436
|
+
},
|
|
437
|
+
onerror: (error: any) => {
|
|
438
|
+
callbacks.onError?.(new Error(error?.message || '请求失败'));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
);
|
|
442
|
+
} catch (error: any) {
|
|
443
|
+
if (error?.name === 'AbortError') {
|
|
444
|
+
callbacks.onMessage?.(fullContent || '', true, { status: 'Cancelled' });
|
|
445
|
+
} else {
|
|
446
|
+
callbacks.onError?.(error);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* 上传文件
|
|
453
|
+
*/
|
|
454
|
+
async upload(file: File): Promise<string> {
|
|
455
|
+
if (!this.isInitialized) {
|
|
456
|
+
throw new Error('请先调用 setup() 初始化SDK');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const domain = this.setupConfig?.domain || '';
|
|
460
|
+
const integrateId = this.config?.integrateId || '';
|
|
461
|
+
|
|
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');
|
|
496
|
+
|
|
497
|
+
// 保留最后一行(可能不完整)
|
|
498
|
+
buffer = lines.pop() || '';
|
|
499
|
+
|
|
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
|
+
}
|
|
516
|
+
}
|
|
517
|
+
} catch (e) {
|
|
518
|
+
// 忽略解析错误,继续处理下一行
|
|
519
|
+
console.debug('解析SSE行失败:', line, e);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
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
|
+
}
|
|
539
|
+
}
|
|
540
|
+
} catch (e) {
|
|
541
|
+
console.debug('解析SSE剩余数据失败:', buffer, e);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (!uploadInfo?.uploadUrl) {
|
|
546
|
+
console.error('获取上传URL失败,uploadInfo:', uploadInfo);
|
|
547
|
+
throw new Error('获取上传URL失败');
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// 上传文件
|
|
551
|
+
const blob = new Blob([file], { type: file.type || 'application/octet-stream' });
|
|
552
|
+
await fetchUploadApi(blob, uploadInfo.uploadUrl);
|
|
553
|
+
|
|
554
|
+
// 确保返回有效的downloadUrl
|
|
555
|
+
if (!uploadInfo.downloadUrl) {
|
|
556
|
+
console.error('downloadUrl为空,uploadInfo:', uploadInfo);
|
|
557
|
+
throw new Error('获取下载URL失败');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return uploadInfo.downloadUrl;
|
|
561
|
+
} catch (error) {
|
|
562
|
+
console.error('上传文件失败:', error);
|
|
563
|
+
throw error;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* 清除会话
|
|
569
|
+
*/
|
|
570
|
+
clear(): void {
|
|
571
|
+
this.sessionId = '';
|
|
572
|
+
this.cancel();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* 取消当前请求
|
|
577
|
+
*/
|
|
578
|
+
cancel(): void {
|
|
579
|
+
if (this.currentController) {
|
|
580
|
+
this.currentController.abort();
|
|
581
|
+
this.currentController = null;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* 获取当前配置
|
|
587
|
+
*/
|
|
588
|
+
getConfig(): ChatConfig | null {
|
|
589
|
+
return this.config;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* 获取当前会话ID
|
|
594
|
+
*/
|
|
595
|
+
getSessionId(): string {
|
|
596
|
+
return this.sessionId;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* 获取指定模型的能力配置
|
|
601
|
+
* @param modelId 模型ID,不传则使用第一个模型
|
|
602
|
+
*/
|
|
603
|
+
getModelCapabilities(modelId?: string): ModelCapabilities {
|
|
604
|
+
if (!this.config) {
|
|
605
|
+
return { image: false, file: false, audio: false, webSearch: false };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const model = modelId
|
|
609
|
+
? this.config.models.find(m => m.id === modelId)
|
|
610
|
+
: this.config.models[0];
|
|
611
|
+
|
|
612
|
+
if (!model) {
|
|
613
|
+
return { image: false, file: false, audio: false, webSearch: false };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// 解析fileConfig
|
|
617
|
+
let fileConfig: ModelCapabilities['fileConfig'] = undefined;
|
|
618
|
+
if (model.config?.fileConfig) {
|
|
619
|
+
try {
|
|
620
|
+
fileConfig = JSON.parse(model.config.fileConfig);
|
|
621
|
+
} catch (e) {
|
|
622
|
+
// 忽略解析错误
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return {
|
|
627
|
+
// 图片:需要全局开启 + 模型支持
|
|
628
|
+
image: Boolean(this.config.features.image && model.config?.image),
|
|
629
|
+
// 文件:需要全局开启 + 模型支持
|
|
630
|
+
file: Boolean(this.config.features.file && model.config?.file),
|
|
631
|
+
// 语音:只需要全局开启
|
|
632
|
+
audio: Boolean(this.config.features.audio),
|
|
633
|
+
// 联网搜索:模型支持即可
|
|
634
|
+
webSearch: Boolean(model.config?.webSearch),
|
|
635
|
+
// 文件配置
|
|
636
|
+
fileConfig,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* 检查指定模型是否支持某个功能
|
|
642
|
+
* @param capability 功能名称
|
|
643
|
+
* @param modelId 模型ID,不传则使用第一个模型
|
|
644
|
+
*/
|
|
645
|
+
hasCapability(capability: 'image' | 'file' | 'audio' | 'webSearch', modelId?: string): boolean {
|
|
646
|
+
const caps = this.getModelCapabilities(modelId);
|
|
647
|
+
return caps[capability];
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* 设置当前会话ID
|
|
652
|
+
* @param sessionId 会话ID
|
|
653
|
+
*/
|
|
654
|
+
setSessionId(sessionId: string): void {
|
|
655
|
+
this.sessionId = sessionId;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* 获取对话历史记录
|
|
660
|
+
* @param sessionId 会话ID,不传则使用当前会话ID
|
|
661
|
+
* @returns 历史消息列表
|
|
662
|
+
*/
|
|
663
|
+
async getHistory(sessionId?: string): Promise<HistoryMessage[]> {
|
|
664
|
+
if (!this.isInitialized) {
|
|
665
|
+
throw new Error('请先调用 setup() 初始化SDK');
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const domain = this.setupConfig?.domain || '';
|
|
669
|
+
const integrateId = this.config?.integrateId || '';
|
|
670
|
+
const targetSessionId = sessionId || this.sessionId;
|
|
671
|
+
|
|
672
|
+
try {
|
|
673
|
+
const { token, ticket } = await this.getRequestToken();
|
|
674
|
+
|
|
675
|
+
const response = await fetch(`${domain}/webhook/chatbot/integrate/${integrateId}/message`, {
|
|
676
|
+
method: 'POST',
|
|
677
|
+
headers: {
|
|
678
|
+
'Content-Type': 'application/json',
|
|
679
|
+
'X-Request-Token': token,
|
|
680
|
+
'X-Account-Session-Ticket': ticket,
|
|
681
|
+
},
|
|
682
|
+
body: targetSessionId ? JSON.stringify({ sessionId: targetSessionId }) : undefined,
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
const result = await response.json();
|
|
686
|
+
|
|
687
|
+
if (result?.httpStatusCode === 200 && result?.data) {
|
|
688
|
+
// 解析历史消息
|
|
689
|
+
const messages: HistoryMessage[] = [];
|
|
690
|
+
const data = result.data;
|
|
691
|
+
|
|
692
|
+
// 处理消息列表
|
|
693
|
+
if (Array.isArray(data)) {
|
|
694
|
+
for (const item of data) {
|
|
695
|
+
// 用户消息 - 在 message 字段中
|
|
696
|
+
if (item.message !== undefined && item.message !== null) {
|
|
697
|
+
const messageType = item.messageType || 'text';
|
|
698
|
+
let content = '';
|
|
699
|
+
let images: string[] | undefined;
|
|
700
|
+
let files: { name: string; url: string }[] | undefined;
|
|
701
|
+
|
|
702
|
+
if (messageType === 'rich' && Array.isArray(item.message)) {
|
|
703
|
+
// rich类型消息:message是数组
|
|
704
|
+
const textParts: string[] = [];
|
|
705
|
+
const imageUrls: string[] = [];
|
|
706
|
+
const fileList: { name: string; url: string }[] = [];
|
|
707
|
+
|
|
708
|
+
for (const part of item.message) {
|
|
709
|
+
if (part.type === 'text' && part.content) {
|
|
710
|
+
textParts.push(part.content);
|
|
711
|
+
} else if (part.type === 'image' && part.mediaUrl) {
|
|
712
|
+
imageUrls.push(part.mediaUrl);
|
|
713
|
+
} else if (part.type === 'file' && part.mediaUrl) {
|
|
714
|
+
fileList.push({
|
|
715
|
+
name: part.mediaName || '文件',
|
|
716
|
+
url: part.mediaUrl,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
content = textParts.join('\n');
|
|
722
|
+
if (imageUrls.length > 0) images = imageUrls;
|
|
723
|
+
if (fileList.length > 0) files = fileList;
|
|
724
|
+
} else if (typeof item.message === 'string') {
|
|
725
|
+
// text类型消息:message是字符串
|
|
726
|
+
content = item.message;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
messages.push({
|
|
730
|
+
id: item.id || `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
731
|
+
role: 'user',
|
|
732
|
+
content,
|
|
733
|
+
messageType,
|
|
734
|
+
gmtCreate: item.gmtCreate,
|
|
735
|
+
sessionId: item.sessionId,
|
|
736
|
+
images,
|
|
737
|
+
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,
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return messages;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return [];
|
|
761
|
+
} catch (error) {
|
|
762
|
+
console.error('获取历史记录失败:', error);
|
|
763
|
+
throw error;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* 获取会话列表
|
|
769
|
+
* @returns 会话列表
|
|
770
|
+
*/
|
|
771
|
+
async getSessions(): Promise<ChatSession[]> {
|
|
772
|
+
if (!this.isInitialized) {
|
|
773
|
+
throw new Error('请先调用 setup() 初始化SDK');
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const domain = this.setupConfig?.domain || '';
|
|
777
|
+
const integrateId = this.config?.integrateId || '';
|
|
778
|
+
|
|
779
|
+
try {
|
|
780
|
+
const { token, ticket } = await this.getRequestToken();
|
|
781
|
+
|
|
782
|
+
// 使用 /message 接口,不传 sessionId 时返回会话列表
|
|
783
|
+
const response = await fetch(`${domain}/webhook/chatbot/integrate/${integrateId}/message`, {
|
|
784
|
+
method: 'POST',
|
|
785
|
+
headers: {
|
|
786
|
+
'Content-Type': 'application/json',
|
|
787
|
+
'X-Request-Token': token,
|
|
788
|
+
'X-Account-Session-Ticket': ticket,
|
|
789
|
+
},
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
const result = await response.json();
|
|
793
|
+
|
|
794
|
+
if (result?.httpStatusCode === 200 && result?.data) {
|
|
795
|
+
const sessions: ChatSession[] = (result.data || []).map((item: any) => ({
|
|
796
|
+
id: item.id,
|
|
797
|
+
sessionId: item.sessionId,
|
|
798
|
+
title: item.title || '未命名会话',
|
|
799
|
+
gmtCreate: item.gmtCreate,
|
|
800
|
+
gmtModified: item.gmtModified,
|
|
801
|
+
}));
|
|
802
|
+
|
|
803
|
+
return sessions;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return [];
|
|
807
|
+
} catch (error) {
|
|
808
|
+
console.error('获取会话列表失败:', error);
|
|
809
|
+
throw error;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* 发送事件回调消息
|
|
815
|
+
* 用于 humanVerify、cardCallBack 等需要向服务端发送回调的场景
|
|
816
|
+
*
|
|
817
|
+
* @param data 回调数据
|
|
818
|
+
* @param data.sessionWebhook 会话 Webhook URL
|
|
819
|
+
* @param data.content 回调内容对象
|
|
820
|
+
* @returns Promise<void>
|
|
821
|
+
*
|
|
822
|
+
* @example
|
|
823
|
+
* ```typescript
|
|
824
|
+
* await chatService.sendEventCallback({
|
|
825
|
+
* sessionWebhook: 'https://...',
|
|
826
|
+
* content: {
|
|
827
|
+
* verifyId: 'xxx',
|
|
828
|
+
* status: 'approve',
|
|
829
|
+
* customParams: { ... }
|
|
830
|
+
* }
|
|
831
|
+
* });
|
|
832
|
+
* ```
|
|
833
|
+
*/
|
|
834
|
+
async sendEventCallback(data: {
|
|
835
|
+
sessionWebhook: string;
|
|
836
|
+
content: Record<string, any>;
|
|
837
|
+
}): Promise<void> {
|
|
838
|
+
if (!this.isInitialized) {
|
|
839
|
+
throw new Error('请先调用 setup() 初始化SDK');
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const { sessionWebhook, content } = data;
|
|
843
|
+
|
|
844
|
+
if (!sessionWebhook) {
|
|
845
|
+
throw new Error('sessionWebhook 不能为空');
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
try {
|
|
849
|
+
const { token, ticket } = await this.getRequestToken();
|
|
850
|
+
|
|
851
|
+
const response = await fetch(sessionWebhook, {
|
|
852
|
+
method: 'POST',
|
|
853
|
+
headers: {
|
|
854
|
+
'Content-Type': 'application/json',
|
|
855
|
+
'X-Request-Token': token,
|
|
856
|
+
'X-Account-Session-Ticket': ticket,
|
|
857
|
+
},
|
|
858
|
+
body: JSON.stringify(content),
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
if (!response.ok) {
|
|
862
|
+
const errorText = await response.text();
|
|
863
|
+
throw new Error(`发送事件回调失败: ${response.status} ${errorText}`);
|
|
864
|
+
}
|
|
865
|
+
} catch (error) {
|
|
866
|
+
console.error('发送事件回调失败:', error);
|
|
867
|
+
throw error;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* 提交人工审核结果
|
|
873
|
+
* 这是 sendEventCallback 的便捷封装,专门用于 HumanVerify 场景
|
|
874
|
+
*
|
|
875
|
+
* @param data 审核数据
|
|
876
|
+
* @param data.verifyId 验证ID
|
|
877
|
+
* @param data.sessionWebhook 会话 Webhook URL
|
|
878
|
+
* @param data.status 审核状态 ('approve' | 'reject')
|
|
879
|
+
* @param data.customParams 自定义参数值
|
|
880
|
+
* @param data.customParamsKey 自定义参数的字段名,默认为 'customParams'
|
|
881
|
+
* @returns Promise<void>
|
|
882
|
+
*
|
|
883
|
+
* @example
|
|
884
|
+
* ```typescript
|
|
885
|
+
* await chatService.submitHumanVerify({
|
|
886
|
+
* verifyId: 'xxx',
|
|
887
|
+
* sessionWebhook: 'https://...',
|
|
888
|
+
* status: 'approve',
|
|
889
|
+
* customParams: { name: '张三', age: 25 },
|
|
890
|
+
* customParamsKey: 'formData'
|
|
891
|
+
* });
|
|
892
|
+
* ```
|
|
893
|
+
*/
|
|
894
|
+
async submitHumanVerify(data: {
|
|
895
|
+
verifyId: string;
|
|
896
|
+
sessionWebhook: string;
|
|
897
|
+
status: 'approve' | 'reject';
|
|
898
|
+
customParams?: Record<string, any>;
|
|
899
|
+
customParamsKey?: string;
|
|
900
|
+
}): Promise<void> {
|
|
901
|
+
const { verifyId, sessionWebhook, status, customParams, customParamsKey = 'customParams' } = data;
|
|
902
|
+
|
|
903
|
+
if (!verifyId) {
|
|
904
|
+
throw new Error('verifyId 不能为空');
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const content: Record<string, any> = {
|
|
908
|
+
verifyId,
|
|
909
|
+
status,
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
// 如果有自定义参数,添加到 content 中
|
|
913
|
+
if (customParams) {
|
|
914
|
+
content[customParamsKey] = customParams;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
return this.sendEventCallback({
|
|
918
|
+
sessionWebhook,
|
|
919
|
+
content,
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// 导出单例
|
|
925
|
+
export const chatService = new ChatService();
|
|
926
|
+
export default ChatService;
|