ai-chat-ui-kit 0.1.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/.eslintrc.cjs +74 -0
- package/.github/actions/screenshot/action.yml +35 -0
- package/.github/workflows/pages.yml +46 -0
- package/README.md +285 -0
- package/docs/README.md +176 -0
- package/docs/api/components.md +344 -0
- package/docs/api/core.md +349 -0
- package/docs/chat-style-1-minimal.html +78 -0
- package/docs/chat-style-2-neon.html +74 -0
- package/docs/chat-style-3-glass.html +73 -0
- package/docs/chat-style-4-terminal.html +84 -0
- package/docs/chat-style-5-gradient.html +69 -0
- package/docs/chat-style-6-corporate.html +116 -0
- package/docs/examples/basic-chat.md +291 -0
- package/docs/examples/custom-plugins.md +431 -0
- package/docs/examples/multi-model.md +466 -0
- package/docs/guide/api-adapters.md +431 -0
- package/docs/guide/getting-started.md +244 -0
- package/docs/guide/headless-mode.md +508 -0
- package/docs/guide/plugins.md +416 -0
- package/docs/guide/themes.md +327 -0
- package/docs/index.html +256 -0
- package/docs/theme-preview-1-minimal.html +74 -0
- package/docs/theme-preview-2-neon.html +73 -0
- package/docs/theme-preview-3-glass.html +77 -0
- package/docs/theme-preview-4-terminal.html +86 -0
- package/docs/theme-preview-5-gradient.html +79 -0
- package/docs/theme-preview-6-corporate.html +71 -0
- package/examples/index.html +414 -0
- package/examples/react-app/App.tsx +131 -0
- package/examples/react-app/index.html +12 -0
- package/examples/react-app/main.tsx +15 -0
- package/examples/react-app/package.json +24 -0
- package/examples/vue-app/index.html +12 -0
- package/examples/vue-app/package.json +22 -0
- package/examples/vue-app/src/App.vue +145 -0
- package/examples/vue-app/src/main.ts +9 -0
- package/package.json +44 -0
- package/packages/components/package.json +25 -0
- package/packages/components/src/chat/chat.css +80 -0
- package/packages/components/src/chat/chat.ts +236 -0
- package/packages/components/src/index.ts +36 -0
- package/packages/components/src/input/input.css +52 -0
- package/packages/components/src/input/input.ts +116 -0
- package/packages/components/src/markdown/markdown.css +118 -0
- package/packages/components/src/markdown/markdown.ts +229 -0
- package/packages/components/src/message/message.css +56 -0
- package/packages/components/src/message/message.ts +72 -0
- package/packages/components/src/styles/global.css +43 -0
- package/packages/components/src/tool-call/tool-call.css +98 -0
- package/packages/components/src/tool-call/tool-call.ts +171 -0
- package/packages/components/src/types.ts +55 -0
- package/packages/components/src/utils/helpers.ts +128 -0
- package/packages/components/tsconfig.json +25 -0
- package/packages/components/tsup.config.ts +18 -0
- package/packages/core/package.json +47 -0
- package/packages/core/pnpm-lock.yaml +2032 -0
- package/packages/core/pnpm-workspace.yaml +2 -0
- package/packages/core/src/api/adapters.ts +717 -0
- package/packages/core/src/api/base.ts +210 -0
- package/packages/core/src/api/index.ts +54 -0
- package/packages/core/src/index.ts +93 -0
- package/packages/core/src/parser/latex.ts +274 -0
- package/packages/core/src/parser/markdown.test.ts +58 -0
- package/packages/core/src/parser/markdown.ts +206 -0
- package/packages/core/src/parser/mermaid.ts +276 -0
- package/packages/core/src/plugins/PluginManager.ts +232 -0
- package/packages/core/src/plugins/builtin.ts +406 -0
- package/packages/core/src/store/ChatStore.ts +163 -0
- package/packages/core/src/store/ModelConfigStore.ts +136 -0
- package/packages/core/src/store/ToolCallStore.ts +164 -0
- package/packages/core/src/store/base.ts +75 -0
- package/packages/core/src/types/index.ts +133 -0
- package/packages/core/tsup.config.ts +18 -0
- package/packages/themes/package.json +33 -0
- package/packages/themes/src/corporate/index.ts +52 -0
- package/packages/themes/src/corporate/theme.css +228 -0
- package/packages/themes/src/glass/index.ts +52 -0
- package/packages/themes/src/glass/theme.css +237 -0
- package/packages/themes/src/gradient/index.ts +53 -0
- package/packages/themes/src/gradient/theme.css +218 -0
- package/packages/themes/src/index.ts +13 -0
- package/packages/themes/src/minimal/index.ts +52 -0
- package/packages/themes/src/minimal/theme.css +198 -0
- package/packages/themes/src/neon/index.ts +52 -0
- package/packages/themes/src/neon/theme.css +233 -0
- package/packages/themes/src/terminal/index.ts +52 -0
- package/packages/themes/src/terminal/theme.css +235 -0
- package/packages/themes/src/types.ts +10 -0
- package/packages/themes/src/vite-env.d.ts +9 -0
- package/packages/themes/tsup.config.ts +21 -0
- package/pnpm-workspace.yaml +4 -0
- package/tsconfig.json +27 -0
- package/vite.config.ts +25 -0
- package/vitest.config.ts +28 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @generated-by AI: edenxpzhang
|
|
3
|
+
* @generated-date 2026-05-13
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { BaseAPIAdapter } from './base.js';
|
|
7
|
+
import { MessageRole, Message, ModelConfig, ChatConfig, ToolCall } from '../types/index.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* OpenAI 兼容适配器
|
|
11
|
+
* 支持 OpenAI 格式(包括文心、通义等兼容接口)
|
|
12
|
+
*/
|
|
13
|
+
export class OpenAIAdapter extends BaseAPIAdapter {
|
|
14
|
+
async sendMessage(params: {
|
|
15
|
+
messages: Message[];
|
|
16
|
+
modelConfig: ModelConfig;
|
|
17
|
+
chatConfig: ChatConfig;
|
|
18
|
+
onChunk?: (chunk: string) => void;
|
|
19
|
+
onToolCall?: (toolCall: ToolCall) => void;
|
|
20
|
+
onComplete?: (message: Message) => void;
|
|
21
|
+
onError?: (error: Error) => void;
|
|
22
|
+
}): Promise<void> {
|
|
23
|
+
const { messages, modelConfig, chatConfig, onChunk, onComplete, onError } = params;
|
|
24
|
+
const controller = this.createAbortController();
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const headers = this.buildHeaders(modelConfig);
|
|
28
|
+
const body = this.buildRequestBody(messages, modelConfig, chatConfig);
|
|
29
|
+
|
|
30
|
+
const response = await fetch(modelConfig.apiEndpoint || 'https://api.openai.com/v1/chat/completions', {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers,
|
|
33
|
+
body: JSON.stringify(body),
|
|
34
|
+
signal: controller.signal
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
const errorText = await response.text();
|
|
39
|
+
throw new Error(`API request failed: ${response.status} ${errorText}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (chatConfig.stream !== false) {
|
|
43
|
+
// 流式响应
|
|
44
|
+
const fullContent = await this.handleStreamResponse(response, (chunk) => {
|
|
45
|
+
if (onChunk) {
|
|
46
|
+
onChunk(chunk);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (onComplete) {
|
|
51
|
+
onComplete({
|
|
52
|
+
id: `msg_${Date.now()}`,
|
|
53
|
+
role: MessageRole.Assistant,
|
|
54
|
+
content: fullContent,
|
|
55
|
+
timestamp: Date.now()
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
// 非流式响应
|
|
60
|
+
const data = await response.json();
|
|
61
|
+
const content = this.extractContentFromNonStreamResponse(data);
|
|
62
|
+
|
|
63
|
+
if (onComplete) {
|
|
64
|
+
onComplete({
|
|
65
|
+
id: `msg_${Date.now()}`,
|
|
66
|
+
role: MessageRole.Assistant,
|
|
67
|
+
content,
|
|
68
|
+
timestamp: Date.now()
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
74
|
+
return; // 正常中止
|
|
75
|
+
}
|
|
76
|
+
this.handleError(error, onError);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
protected buildHeaders(modelConfig: ModelConfig): Record<string, string> {
|
|
81
|
+
const headers = super.buildHeaders(modelConfig);
|
|
82
|
+
|
|
83
|
+
if (modelConfig.apiKey) {
|
|
84
|
+
headers['Authorization'] = `Bearer ${modelConfig.apiKey}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return headers;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
protected extractContentFromChunk(chunk: unknown): string {
|
|
91
|
+
if (typeof chunk === 'object' && chunk !== null) {
|
|
92
|
+
const data = chunk as Record<string, unknown>;
|
|
93
|
+
|
|
94
|
+
if (data.choices && Array.isArray(data.choices) && data.choices.length > 0) {
|
|
95
|
+
const choice = data.choices[0] as Record<string, unknown>;
|
|
96
|
+
const delta = choice.delta as Record<string, unknown> | undefined;
|
|
97
|
+
|
|
98
|
+
if (delta && typeof delta.content === 'string') {
|
|
99
|
+
return delta.content;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private extractContentFromNonStreamResponse(data: unknown): string {
|
|
108
|
+
if (typeof data === 'object' && data !== null) {
|
|
109
|
+
const response = data as Record<string, unknown>;
|
|
110
|
+
|
|
111
|
+
if (response.choices && Array.isArray(response.choices) && response.choices.length > 0) {
|
|
112
|
+
const choice = response.choices[0] as Record<string, unknown>;
|
|
113
|
+
const message = choice.message as Record<string, unknown> | undefined;
|
|
114
|
+
|
|
115
|
+
if (message && typeof message.content === 'string') {
|
|
116
|
+
return message.content;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 百度文心一言适配器
|
|
127
|
+
* 需要特殊处理 access_token
|
|
128
|
+
*/
|
|
129
|
+
export class ERNIEAdapter extends BaseAPIAdapter {
|
|
130
|
+
private accessToken: string | null = null;
|
|
131
|
+
private tokenExpiry: number = 0;
|
|
132
|
+
|
|
133
|
+
async sendMessage(params: {
|
|
134
|
+
messages: Message[];
|
|
135
|
+
modelConfig: ModelConfig;
|
|
136
|
+
chatConfig: ChatConfig;
|
|
137
|
+
onChunk?: (chunk: string) => void;
|
|
138
|
+
onToolCall?: (toolCall: ToolCall) => void;
|
|
139
|
+
onComplete?: (message: Message) => void;
|
|
140
|
+
onError?: (error: Error) => void;
|
|
141
|
+
}): Promise<void> {
|
|
142
|
+
const { messages, modelConfig, chatConfig, onChunk, onComplete, onError } = params;
|
|
143
|
+
const controller = this.createAbortController();
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
// 获取 access_token
|
|
147
|
+
const accessToken = await this.getAccessToken(modelConfig);
|
|
148
|
+
|
|
149
|
+
// 构建文心专用的请求格式
|
|
150
|
+
const ernieMessages = this.convertToERNIEFormat(messages);
|
|
151
|
+
const body = {
|
|
152
|
+
messages: ernieMessages,
|
|
153
|
+
stream: chatConfig.stream !== false,
|
|
154
|
+
temperature: modelConfig.params?.temperature || 0.7,
|
|
155
|
+
top_p: modelConfig.params?.topP || 1,
|
|
156
|
+
penalty_score: 1
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const endpoint = `${modelConfig.apiEndpoint}?access_token=${accessToken}`;
|
|
160
|
+
|
|
161
|
+
const response = await fetch(endpoint, {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: {
|
|
164
|
+
'Content-Type': 'application/json'
|
|
165
|
+
},
|
|
166
|
+
body: JSON.stringify(body),
|
|
167
|
+
signal: controller.signal
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
const errorText = await response.text();
|
|
172
|
+
throw new Error(`ERNIE API request failed: ${response.status} ${errorText}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (chatConfig.stream !== false) {
|
|
176
|
+
const fullContent = await this.handleERNIEStreamResponse(response, (chunk) => {
|
|
177
|
+
if (onChunk) {
|
|
178
|
+
onChunk(chunk);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (onComplete) {
|
|
183
|
+
onComplete({
|
|
184
|
+
id: `msg_${Date.now()}`,
|
|
185
|
+
role: MessageRole.Assistant,
|
|
186
|
+
content: fullContent,
|
|
187
|
+
timestamp: Date.now()
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
const data = await response.json();
|
|
192
|
+
const content = data.result || '';
|
|
193
|
+
|
|
194
|
+
if (onComplete) {
|
|
195
|
+
onComplete({
|
|
196
|
+
id: `msg_${Date.now()}`,
|
|
197
|
+
role: MessageRole.Assistant,
|
|
198
|
+
content,
|
|
199
|
+
timestamp: Date.now()
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
this.handleError(error, onError);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 获取百度 access_token
|
|
213
|
+
*/
|
|
214
|
+
private async getAccessToken(modelConfig: ModelConfig): Promise<string> {
|
|
215
|
+
// 检查缓存的 token 是否过期
|
|
216
|
+
if (this.accessToken && Date.now() < this.tokenExpiry) {
|
|
217
|
+
return this.accessToken;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const apiKey = modelConfig.apiKey;
|
|
221
|
+
const secretKey = modelConfig.headers?.['X-Secret-Key'] as string | undefined;
|
|
222
|
+
|
|
223
|
+
if (!apiKey || !secretKey) {
|
|
224
|
+
throw new Error('ERNIE adapter requires both apiKey (API Key) and X-Secret-Key (Secret Key)');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const tokenUrl = `https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${apiKey}&client_secret=${secretKey}`;
|
|
228
|
+
|
|
229
|
+
const response = await fetch(tokenUrl);
|
|
230
|
+
if (!response.ok) {
|
|
231
|
+
throw new Error(`Failed to get ERNIE access token: ${response.status}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const data = await response.json();
|
|
235
|
+
this.accessToken = data.access_token;
|
|
236
|
+
this.tokenExpiry = Date.now() + (data.expires_in - 300) * 1000; // 提前 5 分钟过期
|
|
237
|
+
|
|
238
|
+
if (!this.accessToken) {
|
|
239
|
+
throw new Error('Failed to retrieve access token');
|
|
240
|
+
}
|
|
241
|
+
return this.accessToken;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 转换为文心格式
|
|
246
|
+
*/
|
|
247
|
+
private convertToERNIEFormat(messages: Message[]): Array<{ role: string; content: string }> {
|
|
248
|
+
return messages.map(msg => ({
|
|
249
|
+
role: msg.role === MessageRole.Tool ? MessageRole.Assistant : msg.role,
|
|
250
|
+
content: msg.content
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 处理文心流式响应(格式与 OpenAI 不同)
|
|
256
|
+
*/
|
|
257
|
+
private async handleERNIEStreamResponse(
|
|
258
|
+
response: Response,
|
|
259
|
+
onChunk: (chunk: string) => void
|
|
260
|
+
): Promise<string> {
|
|
261
|
+
const reader = response.body!.getReader();
|
|
262
|
+
const decoder = new TextDecoder('utf-8');
|
|
263
|
+
let fullContent = '';
|
|
264
|
+
let buffer = '';
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
while (true) {
|
|
268
|
+
const { done, value } = await reader.read();
|
|
269
|
+
|
|
270
|
+
if (done) {
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
buffer += decoder.decode(value, { stream: true });
|
|
275
|
+
const lines = buffer.split('\n');
|
|
276
|
+
buffer = lines.pop() || '';
|
|
277
|
+
|
|
278
|
+
for (const line of lines) {
|
|
279
|
+
const trimmedLine = line.trim();
|
|
280
|
+
|
|
281
|
+
if (!trimmedLine || trimmedLine === 'data: [DONE]') {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (trimmedLine.startsWith('data: ')) {
|
|
286
|
+
const data = trimmedLine.slice(6);
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
const parsed = JSON.parse(data);
|
|
290
|
+
const content = parsed.result || parsed.data || '';
|
|
291
|
+
|
|
292
|
+
if (content) {
|
|
293
|
+
fullContent += content;
|
|
294
|
+
onChunk(content);
|
|
295
|
+
}
|
|
296
|
+
} catch (e) {
|
|
297
|
+
console.warn('Failed to parse ERNIE SSE chunk:', data);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
} finally {
|
|
303
|
+
reader.releaseLock();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return fullContent;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 阿里通义千问适配器
|
|
312
|
+
* 使用 DashScope 格式
|
|
313
|
+
*/
|
|
314
|
+
export class QwenAdapter extends BaseAPIAdapter {
|
|
315
|
+
async sendMessage(params: {
|
|
316
|
+
messages: Message[];
|
|
317
|
+
modelConfig: ModelConfig;
|
|
318
|
+
chatConfig: ChatConfig;
|
|
319
|
+
onChunk?: (chunk: string) => void;
|
|
320
|
+
onToolCall?: (toolCall: ToolCall) => void;
|
|
321
|
+
onComplete?: (message: Message) => void;
|
|
322
|
+
onError?: (error: Error) => void;
|
|
323
|
+
}): Promise<void> {
|
|
324
|
+
const { messages, modelConfig, chatConfig, onChunk, onComplete, onError } = params;
|
|
325
|
+
const controller = this.createAbortController();
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
const headers = this.buildHeaders(modelConfig);
|
|
329
|
+
|
|
330
|
+
// 转换为 DashScope 格式
|
|
331
|
+
const dashScopeMessages = this.convertToDashScopeFormat(messages);
|
|
332
|
+
|
|
333
|
+
const body = {
|
|
334
|
+
model: modelConfig.model,
|
|
335
|
+
input: {
|
|
336
|
+
messages: dashScopeMessages
|
|
337
|
+
},
|
|
338
|
+
parameters: {
|
|
339
|
+
temperature: modelConfig.params?.temperature || 0.7,
|
|
340
|
+
top_p: modelConfig.params?.topP || 1,
|
|
341
|
+
max_tokens: modelConfig.params?.maxTokens || 2048,
|
|
342
|
+
result_format: 'message'
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const response = await fetch(modelConfig.apiEndpoint || 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation', {
|
|
347
|
+
method: 'POST',
|
|
348
|
+
headers,
|
|
349
|
+
body: JSON.stringify(body),
|
|
350
|
+
signal: controller.signal
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
if (!response.ok) {
|
|
354
|
+
const errorText = await response.text();
|
|
355
|
+
throw new Error(`Qwen API request failed: ${response.status} ${errorText}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (chatConfig.stream !== false) {
|
|
359
|
+
const fullContent = await this.handleQwenStreamResponse(response, (chunk) => {
|
|
360
|
+
if (onChunk) {
|
|
361
|
+
onChunk(chunk);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
if (onComplete) {
|
|
366
|
+
onComplete({
|
|
367
|
+
id: `msg_${Date.now()}`,
|
|
368
|
+
role: MessageRole.Assistant,
|
|
369
|
+
content: fullContent,
|
|
370
|
+
timestamp: Date.now()
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
const data = await response.json();
|
|
375
|
+
const content = this.extractContentFromDashScopeResponse(data);
|
|
376
|
+
|
|
377
|
+
if (onComplete) {
|
|
378
|
+
onComplete({
|
|
379
|
+
id: `msg_${Date.now()}`,
|
|
380
|
+
role: MessageRole.Assistant,
|
|
381
|
+
content,
|
|
382
|
+
timestamp: Date.now()
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
} catch (error) {
|
|
387
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
this.handleError(error, onError);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
protected buildHeaders(modelConfig: ModelConfig): Record<string, string> {
|
|
395
|
+
const headers: Record<string, string> = {
|
|
396
|
+
'Content-Type': 'application/json'
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
if (modelConfig.apiKey) {
|
|
400
|
+
headers['Authorization'] = `Bearer ${modelConfig.apiKey}`;
|
|
401
|
+
headers['X-DashScope-SLO'] = 'your-slo';
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return headers;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* 转换为 DashScope 格式
|
|
409
|
+
*/
|
|
410
|
+
private convertToDashScopeFormat(messages: Message[]): Array<{ role: string; content: string }> {
|
|
411
|
+
return messages.map(msg => ({
|
|
412
|
+
role: msg.role === MessageRole.Tool ? 'function' : msg.role,
|
|
413
|
+
content: msg.content
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* 处理通义千问流式响应
|
|
419
|
+
*/
|
|
420
|
+
private async handleQwenStreamResponse(
|
|
421
|
+
response: Response,
|
|
422
|
+
onChunk: (chunk: string) => void
|
|
423
|
+
): Promise<string> {
|
|
424
|
+
const reader = response.body!.getReader();
|
|
425
|
+
const decoder = new TextDecoder('utf-8');
|
|
426
|
+
let fullContent = '';
|
|
427
|
+
let buffer = '';
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
while (true) {
|
|
431
|
+
const { done, value } = await reader.read();
|
|
432
|
+
|
|
433
|
+
if (done) {
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
buffer += decoder.decode(value, { stream: true });
|
|
438
|
+
const lines = buffer.split('\n');
|
|
439
|
+
buffer = lines.pop() || '';
|
|
440
|
+
|
|
441
|
+
for (const line of lines) {
|
|
442
|
+
const trimmedLine = line.trim();
|
|
443
|
+
|
|
444
|
+
if (!trimmedLine) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
const parsed = JSON.parse(trimmedLine);
|
|
450
|
+
|
|
451
|
+
if (parsed.output && parsed.output.choices) {
|
|
452
|
+
const choice = parsed.output.choices[0];
|
|
453
|
+
if (choice.message && choice.message.content) {
|
|
454
|
+
const content = choice.message.content;
|
|
455
|
+
fullContent += content;
|
|
456
|
+
onChunk(content);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
} catch (e) {
|
|
460
|
+
console.warn('Failed to parse Qwen SSE chunk:', trimmedLine);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
} finally {
|
|
465
|
+
reader.releaseLock();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return fullContent;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* 从 DashScope 非流式响应中提取内容
|
|
473
|
+
*/
|
|
474
|
+
private extractContentFromDashScopeResponse(data: unknown): string {
|
|
475
|
+
if (typeof data === 'object' && data !== null) {
|
|
476
|
+
const response = data as Record<string, unknown>;
|
|
477
|
+
|
|
478
|
+
if (response.output && typeof response.output === 'object') {
|
|
479
|
+
const output = response.output as Record<string, unknown>;
|
|
480
|
+
|
|
481
|
+
if (output.choices && Array.isArray(output.choices) && output.choices.length > 0) {
|
|
482
|
+
const choice = output.choices[0] as Record<string, unknown>;
|
|
483
|
+
|
|
484
|
+
if (choice.message && typeof choice.message === 'object') {
|
|
485
|
+
const message = choice.message as Record<string, unknown>;
|
|
486
|
+
return typeof message.content === 'string' ? message.content : '';
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return '';
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* 讯飞星火适配器
|
|
498
|
+
*/
|
|
499
|
+
export class SparkAdapter extends BaseAPIAdapter {
|
|
500
|
+
async sendMessage(params: {
|
|
501
|
+
messages: Message[];
|
|
502
|
+
modelConfig: ModelConfig;
|
|
503
|
+
chatConfig: ChatConfig;
|
|
504
|
+
onChunk?: (chunk: string) => void;
|
|
505
|
+
onToolCall?: (toolCall: ToolCall) => void;
|
|
506
|
+
onComplete?: (message: Message) => void;
|
|
507
|
+
onError?: (error: Error) => void;
|
|
508
|
+
}): Promise<void> {
|
|
509
|
+
const { messages, modelConfig, chatConfig, onChunk, onComplete, onError } = params;
|
|
510
|
+
const controller = this.createAbortController();
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
// 星火需要使用 WebSocket 或特定的 HTTP 接口
|
|
514
|
+
// 这里使用 HTTP 接口示例
|
|
515
|
+
const headers = {
|
|
516
|
+
'Content-Type': 'application/json'
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const sparkMessages = messages.map(msg => ({
|
|
520
|
+
role: msg.role,
|
|
521
|
+
content: msg.content
|
|
522
|
+
}));
|
|
523
|
+
|
|
524
|
+
const body = {
|
|
525
|
+
header: {
|
|
526
|
+
app_id: modelConfig.apiKey,
|
|
527
|
+
uid: `user_${Date.now()}`
|
|
528
|
+
},
|
|
529
|
+
parameter: {
|
|
530
|
+
chat: {
|
|
531
|
+
domain: modelConfig.model || 'general',
|
|
532
|
+
temperature: modelConfig.params?.temperature || 0.7,
|
|
533
|
+
max_tokens: modelConfig.params?.maxTokens || 2048
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
payload: {
|
|
537
|
+
message: {
|
|
538
|
+
text: sparkMessages
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const response = await fetch(modelConfig.apiEndpoint || 'https://spark-api.xf-yun.com/v1/chat/completions', {
|
|
544
|
+
method: 'POST',
|
|
545
|
+
headers,
|
|
546
|
+
body: JSON.stringify(body),
|
|
547
|
+
signal: controller.signal
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
if (!response.ok) {
|
|
551
|
+
const errorText = await response.text();
|
|
552
|
+
throw new Error(`Spark API request failed: ${response.status} ${errorText}`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// 处理星火响应(简化版本,实际需要按星火格式解析)
|
|
556
|
+
const data = await response.json();
|
|
557
|
+
const content = this.extractContentFromSparkResponse(data);
|
|
558
|
+
|
|
559
|
+
if (onComplete) {
|
|
560
|
+
onComplete({
|
|
561
|
+
id: `msg_${Date.now()}`,
|
|
562
|
+
role: MessageRole.Assistant,
|
|
563
|
+
content,
|
|
564
|
+
timestamp: Date.now()
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
} catch (error) {
|
|
568
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
this.handleError(error, onError);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* 从星火响应中提取内容
|
|
577
|
+
*/
|
|
578
|
+
private extractContentFromSparkResponse(data: unknown): string {
|
|
579
|
+
if (typeof data === 'object' && data !== null) {
|
|
580
|
+
const response = data as Record<string, unknown>;
|
|
581
|
+
|
|
582
|
+
if (response.payload && typeof response.payload === 'object') {
|
|
583
|
+
const payload = response.payload as Record<string, unknown>;
|
|
584
|
+
|
|
585
|
+
if (payload.choices && typeof payload.choices === 'object') {
|
|
586
|
+
const choices = payload.choices as Record<string, unknown>;
|
|
587
|
+
|
|
588
|
+
if (choices.text && Array.isArray(choices.text) && choices.text.length > 0) {
|
|
589
|
+
const firstChoice = choices.text[0] as Record<string, unknown>;
|
|
590
|
+
return typeof firstChoice.content === 'string' ? firstChoice.content : '';
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return '';
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* 腾讯混元适配器
|
|
602
|
+
*/
|
|
603
|
+
export class HunyuanAdapter extends BaseAPIAdapter {
|
|
604
|
+
async sendMessage(params: {
|
|
605
|
+
messages: Message[];
|
|
606
|
+
modelConfig: ModelConfig;
|
|
607
|
+
chatConfig: ChatConfig;
|
|
608
|
+
onChunk?: (chunk: string) => void;
|
|
609
|
+
onToolCall?: (toolCall: ToolCall) => void;
|
|
610
|
+
onComplete?: (message: Message) => void;
|
|
611
|
+
onError?: (error: Error) => void;
|
|
612
|
+
}): Promise<void> {
|
|
613
|
+
const { messages, modelConfig, chatConfig, onChunk, onComplete, onError } = params;
|
|
614
|
+
const controller = this.createAbortController();
|
|
615
|
+
|
|
616
|
+
try {
|
|
617
|
+
const headers = this.buildHeaders(modelConfig);
|
|
618
|
+
|
|
619
|
+
const body = {
|
|
620
|
+
model: modelConfig.model || 'hunyuan-lite',
|
|
621
|
+
messages: this.convertToOpenAIFormat(messages),
|
|
622
|
+
stream: chatConfig.stream !== false,
|
|
623
|
+
temperature: modelConfig.params?.temperature || 0.7,
|
|
624
|
+
top_p: modelConfig.params?.topP || 1,
|
|
625
|
+
max_tokens: modelConfig.params?.maxTokens || 2048
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const response = await fetch(modelConfig.apiEndpoint || 'https://hunyuan.tencentcloudapi.com/', {
|
|
629
|
+
method: 'POST',
|
|
630
|
+
headers,
|
|
631
|
+
body: JSON.stringify(body),
|
|
632
|
+
signal: controller.signal
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
if (!response.ok) {
|
|
636
|
+
const errorText = await response.text();
|
|
637
|
+
throw new Error(`Hunyuan API request failed: ${response.status} ${errorText}`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (chatConfig.stream !== false) {
|
|
641
|
+
// 混元支持 SSE 格式
|
|
642
|
+
const fullContent = await this.handleStreamResponse(response, (chunk) => {
|
|
643
|
+
if (onChunk) {
|
|
644
|
+
onChunk(chunk);
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
if (onComplete) {
|
|
649
|
+
onComplete({
|
|
650
|
+
id: `msg_${Date.now()}`,
|
|
651
|
+
role: MessageRole.Assistant,
|
|
652
|
+
content: fullContent,
|
|
653
|
+
timestamp: Date.now()
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
} else {
|
|
657
|
+
const data = await response.json();
|
|
658
|
+
const content = this.extractContentFromHunyuanResponse(data);
|
|
659
|
+
|
|
660
|
+
if (onComplete) {
|
|
661
|
+
onComplete({
|
|
662
|
+
id: `msg_${Date.now()}`,
|
|
663
|
+
role: MessageRole.Assistant,
|
|
664
|
+
content,
|
|
665
|
+
timestamp: Date.now()
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
} catch (error) {
|
|
670
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
this.handleError(error, onError);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
protected buildHeaders(modelConfig: ModelConfig): Record<string, string> {
|
|
678
|
+
const headers: Record<string, string> = {
|
|
679
|
+
'Content-Type': 'application/json'
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
// 混元需要使用腾讯云的签名认证
|
|
683
|
+
// 这里简化处理,实际应使用腾讯云 SDK 签名
|
|
684
|
+
if (modelConfig.apiKey) {
|
|
685
|
+
headers['Authorization'] = `Bearer ${modelConfig.apiKey}`;
|
|
686
|
+
headers['X-TC-Action'] = 'ChatCompletions';
|
|
687
|
+
headers['X-TC-Version'] = '2023-09-01';
|
|
688
|
+
headers['X-TC-Region'] = 'ap-guangzhou';
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return headers;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* 从混元响应中提取内容
|
|
696
|
+
*/
|
|
697
|
+
private extractContentFromHunyuanResponse(data: unknown): string {
|
|
698
|
+
if (typeof data === 'object' && data !== null) {
|
|
699
|
+
const response = data as Record<string, unknown>;
|
|
700
|
+
|
|
701
|
+
if (response.Response && typeof response.Response === 'object') {
|
|
702
|
+
const resp = response.Response as Record<string, unknown>;
|
|
703
|
+
|
|
704
|
+
if (resp.Choices && Array.isArray(resp.Choices) && resp.Choices.length > 0) {
|
|
705
|
+
const choice = resp.Choices[0] as Record<string, unknown>;
|
|
706
|
+
const message = choice.Message as Record<string, unknown> | undefined;
|
|
707
|
+
|
|
708
|
+
if (message && typeof message.Content === 'string') {
|
|
709
|
+
return message.Content;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return '';
|
|
716
|
+
}
|
|
717
|
+
}
|