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.
Files changed (95) hide show
  1. package/.eslintrc.cjs +74 -0
  2. package/.github/actions/screenshot/action.yml +35 -0
  3. package/.github/workflows/pages.yml +46 -0
  4. package/README.md +285 -0
  5. package/docs/README.md +176 -0
  6. package/docs/api/components.md +344 -0
  7. package/docs/api/core.md +349 -0
  8. package/docs/chat-style-1-minimal.html +78 -0
  9. package/docs/chat-style-2-neon.html +74 -0
  10. package/docs/chat-style-3-glass.html +73 -0
  11. package/docs/chat-style-4-terminal.html +84 -0
  12. package/docs/chat-style-5-gradient.html +69 -0
  13. package/docs/chat-style-6-corporate.html +116 -0
  14. package/docs/examples/basic-chat.md +291 -0
  15. package/docs/examples/custom-plugins.md +431 -0
  16. package/docs/examples/multi-model.md +466 -0
  17. package/docs/guide/api-adapters.md +431 -0
  18. package/docs/guide/getting-started.md +244 -0
  19. package/docs/guide/headless-mode.md +508 -0
  20. package/docs/guide/plugins.md +416 -0
  21. package/docs/guide/themes.md +327 -0
  22. package/docs/index.html +256 -0
  23. package/docs/theme-preview-1-minimal.html +74 -0
  24. package/docs/theme-preview-2-neon.html +73 -0
  25. package/docs/theme-preview-3-glass.html +77 -0
  26. package/docs/theme-preview-4-terminal.html +86 -0
  27. package/docs/theme-preview-5-gradient.html +79 -0
  28. package/docs/theme-preview-6-corporate.html +71 -0
  29. package/examples/index.html +414 -0
  30. package/examples/react-app/App.tsx +131 -0
  31. package/examples/react-app/index.html +12 -0
  32. package/examples/react-app/main.tsx +15 -0
  33. package/examples/react-app/package.json +24 -0
  34. package/examples/vue-app/index.html +12 -0
  35. package/examples/vue-app/package.json +22 -0
  36. package/examples/vue-app/src/App.vue +145 -0
  37. package/examples/vue-app/src/main.ts +9 -0
  38. package/package.json +44 -0
  39. package/packages/components/package.json +25 -0
  40. package/packages/components/src/chat/chat.css +80 -0
  41. package/packages/components/src/chat/chat.ts +236 -0
  42. package/packages/components/src/index.ts +36 -0
  43. package/packages/components/src/input/input.css +52 -0
  44. package/packages/components/src/input/input.ts +116 -0
  45. package/packages/components/src/markdown/markdown.css +118 -0
  46. package/packages/components/src/markdown/markdown.ts +229 -0
  47. package/packages/components/src/message/message.css +56 -0
  48. package/packages/components/src/message/message.ts +72 -0
  49. package/packages/components/src/styles/global.css +43 -0
  50. package/packages/components/src/tool-call/tool-call.css +98 -0
  51. package/packages/components/src/tool-call/tool-call.ts +171 -0
  52. package/packages/components/src/types.ts +55 -0
  53. package/packages/components/src/utils/helpers.ts +128 -0
  54. package/packages/components/tsconfig.json +25 -0
  55. package/packages/components/tsup.config.ts +18 -0
  56. package/packages/core/package.json +47 -0
  57. package/packages/core/pnpm-lock.yaml +2032 -0
  58. package/packages/core/pnpm-workspace.yaml +2 -0
  59. package/packages/core/src/api/adapters.ts +717 -0
  60. package/packages/core/src/api/base.ts +210 -0
  61. package/packages/core/src/api/index.ts +54 -0
  62. package/packages/core/src/index.ts +93 -0
  63. package/packages/core/src/parser/latex.ts +274 -0
  64. package/packages/core/src/parser/markdown.test.ts +58 -0
  65. package/packages/core/src/parser/markdown.ts +206 -0
  66. package/packages/core/src/parser/mermaid.ts +276 -0
  67. package/packages/core/src/plugins/PluginManager.ts +232 -0
  68. package/packages/core/src/plugins/builtin.ts +406 -0
  69. package/packages/core/src/store/ChatStore.ts +163 -0
  70. package/packages/core/src/store/ModelConfigStore.ts +136 -0
  71. package/packages/core/src/store/ToolCallStore.ts +164 -0
  72. package/packages/core/src/store/base.ts +75 -0
  73. package/packages/core/src/types/index.ts +133 -0
  74. package/packages/core/tsup.config.ts +18 -0
  75. package/packages/themes/package.json +33 -0
  76. package/packages/themes/src/corporate/index.ts +52 -0
  77. package/packages/themes/src/corporate/theme.css +228 -0
  78. package/packages/themes/src/glass/index.ts +52 -0
  79. package/packages/themes/src/glass/theme.css +237 -0
  80. package/packages/themes/src/gradient/index.ts +53 -0
  81. package/packages/themes/src/gradient/theme.css +218 -0
  82. package/packages/themes/src/index.ts +13 -0
  83. package/packages/themes/src/minimal/index.ts +52 -0
  84. package/packages/themes/src/minimal/theme.css +198 -0
  85. package/packages/themes/src/neon/index.ts +52 -0
  86. package/packages/themes/src/neon/theme.css +233 -0
  87. package/packages/themes/src/terminal/index.ts +52 -0
  88. package/packages/themes/src/terminal/theme.css +235 -0
  89. package/packages/themes/src/types.ts +10 -0
  90. package/packages/themes/src/vite-env.d.ts +9 -0
  91. package/packages/themes/tsup.config.ts +21 -0
  92. package/pnpm-workspace.yaml +4 -0
  93. package/tsconfig.json +27 -0
  94. package/vite.config.ts +25 -0
  95. package/vitest.config.ts +28 -0
@@ -0,0 +1,2 @@
1
+ allowBuilds:
2
+ esbuild: set this to true or false
@@ -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
+ }