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,406 @@
1
+ /**
2
+ * @generated-by AI: edenxpzhang
3
+ * @generated-date 2026-05-13
4
+ */
5
+
6
+ import { MessagePlugin, Message, RenderContext, ToolCall } from '../types/index.js';
7
+ import { detectMermaid } from '../parser/mermaid.js';
8
+ import { detectLatex, renderLatex } from '../parser/latex.js';
9
+ import { parseMarkdown } from '../parser/markdown.js';
10
+
11
+ /**
12
+ * 代码块插件
13
+ * 支持代码高亮、复制按钮、语言标签
14
+ */
15
+ export class CodeBlockPlugin implements MessagePlugin {
16
+ type = 'code-block';
17
+ priority = 10;
18
+
19
+ /**
20
+ * 判断是否匹配代码块
21
+ */
22
+ match(content: string): boolean {
23
+ if (!content || typeof content !== 'string') {
24
+ return false;
25
+ }
26
+
27
+ // 检测 ``` 代码块
28
+ const codeBlockRegex = /```[\s\S]*?```/;
29
+ return codeBlockRegex.test(content);
30
+ }
31
+
32
+ /**
33
+ * 渲染代码块
34
+ */
35
+ render(content: string, context: RenderContext): HTMLElement | string {
36
+ if (!content) {
37
+ return '';
38
+ }
39
+
40
+ // 使用 marked 解析 Markdown(它会自动处理代码块高亮)
41
+ const html = parseMarkdown(content);
42
+
43
+ // 创建容器
44
+ const container = document.createElement('div');
45
+ container.className = 'plugin-code-block';
46
+ container.innerHTML = html;
47
+
48
+ // 为代码块添加复制按钮和语言标签
49
+ this.enhanceCodeBlocks(container);
50
+
51
+ return container;
52
+ }
53
+
54
+ /**
55
+ * 增强代码块(添加复制按钮等)
56
+ */
57
+ private enhanceCodeBlocks(container: HTMLElement): void {
58
+ const codeBlocks = container.querySelectorAll('pre code');
59
+
60
+ codeBlocks.forEach((codeEl) => {
61
+ const pre = codeEl.closest('pre');
62
+ if (!pre) return;
63
+
64
+ // 创建包装器
65
+ const wrapper = document.createElement('div');
66
+ wrapper.className = 'code-block-wrapper';
67
+
68
+ // 获取语言
69
+ const classes = codeEl.className;
70
+ const langMatch = classes.match(/language-(\S+)/);
71
+ const language = langMatch ? langMatch[1] : 'plaintext';
72
+
73
+ // 创建头部
74
+ const header = document.createElement('div');
75
+ header.className = 'code-block-header';
76
+
77
+ const langLabel = document.createElement('span');
78
+ langLabel.className = 'code-block-lang';
79
+ langLabel.textContent = language;
80
+
81
+ const copyBtn = document.createElement('button');
82
+ copyBtn.className = 'code-block-copy-btn';
83
+ copyBtn.textContent = '复制';
84
+ copyBtn.addEventListener('click', () => {
85
+ const code = codeEl.textContent || '';
86
+ navigator.clipboard.writeText(code).then(() => {
87
+ copyBtn.textContent = '已复制';
88
+ setTimeout(() => {
89
+ copyBtn.textContent = '复制';
90
+ }, 2000);
91
+ });
92
+ });
93
+
94
+ header.appendChild(langLabel);
95
+ header.appendChild(copyBtn);
96
+
97
+ // 重新组织 DOM
98
+ pre.parentNode?.insertBefore(wrapper, pre);
99
+ wrapper.appendChild(header);
100
+ wrapper.appendChild(pre);
101
+ });
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Mermaid 图表插件
107
+ * 检测并渲染 Mermaid 图表
108
+ */
109
+ export class MermaidPlugin implements MessagePlugin {
110
+ type = 'mermaid';
111
+ priority = 20;
112
+
113
+ /**
114
+ * 判断是否包含 Mermaid 图表
115
+ */
116
+ match(content: string): boolean {
117
+ if (!content || typeof content !== 'string') {
118
+ return false;
119
+ }
120
+
121
+ // 检测 ```mermaid 代码块
122
+ const mermaidBlockRegex = /```mermaid[\s\S]*?```/i;
123
+ if (mermaidBlockRegex.test(content)) {
124
+ return true;
125
+ }
126
+
127
+ // 或使用专门的 mermaid 检测函数
128
+ return detectMermaid(content);
129
+ }
130
+
131
+ /**
132
+ * 渲染 Mermaid 图表
133
+ */
134
+ render(content: string, context: RenderContext): HTMLElement | string {
135
+ if (!content) {
136
+ return '';
137
+ }
138
+
139
+ const container = document.createElement('div');
140
+ container.className = 'plugin-mermaid';
141
+
142
+ // 提取所有 mermaid 代码块
143
+ const mermaidRegex = /```mermaid\s*([\s\S]*?)```/gi;
144
+ let match: RegExpExecArray | null;
145
+ let remainingContent = content;
146
+
147
+ const parts: Array<{ type: 'text' | 'mermaid'; content: string }> = [];
148
+
149
+ while ((match = mermaidRegex.exec(content)) !== null) {
150
+ const mermaidCode = match[1]?.trim() || '';
151
+
152
+ // 添加 mermaid 之前的文本
153
+ const textBefore = content.substring(0, match.index);
154
+ if (textBefore) {
155
+ parts.push({ type: 'text', content: textBefore });
156
+ }
157
+
158
+ // 添加 mermaid 代码
159
+ parts.push({ type: 'mermaid', content: mermaidCode });
160
+
161
+ // 更新剩余内容
162
+ remainingContent = content.substring(match.index + match[0].length);
163
+ }
164
+
165
+ // 添加剩余文本
166
+ if (remainingContent) {
167
+ parts.push({ type: 'text', content: remainingContent });
168
+ }
169
+
170
+ // 渲染各部分
171
+ parts.forEach(part => {
172
+ if (part.type === 'text') {
173
+ const textHtml = parseMarkdown(part.content);
174
+ const textDiv = document.createElement('div');
175
+ textDiv.innerHTML = textHtml;
176
+ container.appendChild(textDiv);
177
+ } else if (part.type === 'mermaid') {
178
+ const mermaidContainer = document.createElement('div');
179
+ mermaidContainer.className = 'mermaid-render-area';
180
+ container.appendChild(mermaidContainer);
181
+
182
+ // 动态导入并渲染 mermaid
183
+ this.renderMermaidDiagram(part.content, mermaidContainer);
184
+ }
185
+ });
186
+
187
+ return container;
188
+ }
189
+
190
+ /**
191
+ * 渲染 Mermaid 图表
192
+ */
193
+ private renderMermaidDiagram(code: string, container: HTMLElement): void {
194
+ import('mermaid')
195
+ .then((mermaid) => {
196
+ mermaid.default.initialize({
197
+ startOnLoad: false,
198
+ theme: 'default',
199
+ securityLevel: 'loose'
200
+ });
201
+
202
+ const id = `mermaid-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
203
+ container.id = id;
204
+ container.textContent = code;
205
+
206
+ mermaid.default.run({ querySelector: `#${id}` });
207
+ })
208
+ .catch((err) => {
209
+ console.error('Failed to load mermaid:', err);
210
+ container.innerHTML = `<pre style="color:red;">Mermaid 渲染失败</pre>`;
211
+ });
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Latex 公式插件
217
+ * 检测并渲染 Latex 公式
218
+ */
219
+ export class LatexPlugin implements MessagePlugin {
220
+ type = 'latex';
221
+ priority = 15;
222
+
223
+ /**
224
+ * 判断是否包含 Latex 公式
225
+ */
226
+ match(content: string): boolean {
227
+ if (!content || typeof content !== 'string') {
228
+ return false;
229
+ }
230
+
231
+ return detectLatex(content).inline.length > 0 || detectLatex(content).block.length > 0;
232
+ }
233
+
234
+ /**
235
+ * 渲染 Latex 公式
236
+ */
237
+ render(content: string, context: RenderContext): HTMLElement | string {
238
+ if (!content) {
239
+ return '';
240
+ }
241
+
242
+ // 使用 KaTeX 或 MathJax 渲染
243
+ const container = document.createElement('div');
244
+ container.className = 'plugin-latex';
245
+
246
+ // 先解析 Markdown
247
+ let html = parseMarkdown(content);
248
+
249
+ // 替换 Latex 占位符为渲染后的公式
250
+ // 这里需要外部库(KaTeX/MathJax)支持
251
+ // 返回带有 class 标记的 HTML,供外部渲染
252
+ container.innerHTML = renderLatex(html);
253
+
254
+ // 尝试使用 KaTeX 渲染
255
+ this.renderWithKaTeX(container);
256
+
257
+ return container;
258
+ }
259
+
260
+ /**
261
+ * 使用 KaTeX 渲染
262
+ */
263
+ private renderWithKaTeX(container: HTMLElement): void {
264
+ import('katex')
265
+ .then((katex) => {
266
+ // 渲染块级公式
267
+ const blockFormulas = container.querySelectorAll('.latex-block');
268
+ blockFormulas.forEach(el => {
269
+ if (!(el instanceof HTMLElement)) return;
270
+ const formula = el.textContent || '';
271
+ try {
272
+ katex.default.render(formula.replace(/^\$|\$$/g, ''), el, {
273
+ displayMode: true,
274
+ throwOnError: false
275
+ });
276
+ } catch (err) {
277
+ console.error('KaTeX block render error:', err);
278
+ }
279
+ });
280
+
281
+ // 渲染行内公式
282
+ const inlineFormulas = container.querySelectorAll('.latex-inline');
283
+ inlineFormulas.forEach(el => {
284
+ if (!(el instanceof HTMLElement)) return;
285
+ const formula = el.textContent || '';
286
+ try {
287
+ katex.default.render(formula.replace(/^\$|\$$/g, ''), el, {
288
+ displayMode: false,
289
+ throwOnError: false
290
+ });
291
+ } catch (err) {
292
+ console.error('KaTeX inline render error:', err);
293
+ }
294
+ });
295
+ })
296
+ .catch((err) => {
297
+ console.warn('KaTeX not loaded:', err);
298
+ });
299
+ }
300
+ }
301
+
302
+ /**
303
+ * 工具调用插件
304
+ * 渲染工具调用状态和结果
305
+ */
306
+ export class ToolCallPlugin implements MessagePlugin {
307
+ type = 'tool-call';
308
+ priority = 5;
309
+
310
+ /**
311
+ * 判断是否包含工具调用
312
+ */
313
+ match(content: string): boolean {
314
+ // 这个插件主要通过 ToolCall 数据触发,而非内容匹配
315
+ return false;
316
+ }
317
+
318
+ /**
319
+ * 渲染工具调用
320
+ */
321
+ render(content: string, context: RenderContext): HTMLElement | string {
322
+ const container = document.createElement('div');
323
+ container.className = 'plugin-tool-call';
324
+
325
+ const message = context.message;
326
+ if (!message || !message.toolCalls || message.toolCalls.length === 0) {
327
+ return '';
328
+ }
329
+
330
+ // 渲染每个工具调用
331
+ message.toolCalls.forEach(toolCall => {
332
+ const toolElement = this.renderToolCall(toolCall);
333
+ container.appendChild(toolElement);
334
+ });
335
+
336
+ return container;
337
+ }
338
+
339
+ /**
340
+ * 渲染单个工具调用
341
+ */
342
+ private renderToolCall(toolCall: ToolCall): HTMLElement {
343
+ const wrapper = document.createElement('div');
344
+ wrapper.className = `tool-call tool-call--${toolCall.status}`;
345
+
346
+ // 头部:工具名称和状态
347
+ const header = document.createElement('div');
348
+ header.className = 'tool-call__header';
349
+
350
+ const name = document.createElement('span');
351
+ name.className = 'tool-call__name';
352
+ name.textContent = toolCall.name;
353
+
354
+ const status = document.createElement('span');
355
+ status.className = 'tool-call__status';
356
+ status.textContent = this.getStatusText(toolCall.status);
357
+
358
+ header.appendChild(name);
359
+ header.appendChild(status);
360
+
361
+ // 参数
362
+ const args = document.createElement('pre');
363
+ args.className = 'tool-call__args';
364
+ args.textContent = JSON.stringify(toolCall.arguments, null, 2);
365
+
366
+ wrapper.appendChild(header);
367
+ wrapper.appendChild(args);
368
+
369
+ // 结果(如果有)
370
+ if (toolCall.result !== undefined) {
371
+ const result = document.createElement('pre');
372
+ result.className = 'tool-call__result';
373
+ result.textContent = typeof toolCall.result === 'string'
374
+ ? toolCall.result
375
+ : JSON.stringify(toolCall.result, null, 2);
376
+ wrapper.appendChild(result);
377
+ }
378
+
379
+ return wrapper;
380
+ }
381
+
382
+ /**
383
+ * 获取状态文本
384
+ */
385
+ private getStatusText(status: ToolCall['status']): string {
386
+ const statusMap: Record<ToolCall['status'], string> = {
387
+ pending: '等待中',
388
+ executing: '执行中',
389
+ completed: '已完成',
390
+ failed: '失败'
391
+ };
392
+
393
+ return statusMap[status] || status;
394
+ }
395
+ }
396
+
397
+ /**
398
+ * 注册所有内置插件到插件管理器
399
+ * @param manager 插件管理器实例
400
+ */
401
+ export function registerBuiltinPlugins(manager: import('./PluginManager.js').PluginManager): void {
402
+ manager.register(new CodeBlockPlugin());
403
+ manager.register(new MermaidPlugin());
404
+ manager.register(new LatexPlugin());
405
+ manager.register(new ToolCallPlugin());
406
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * @generated-by AI: edenxpzhang
3
+ * @generated-date 2026-05-13
4
+ */
5
+
6
+ import { BaseStore } from './base.js';
7
+ import { ChatState, Message, MessageRole, ChatConfig } from '../types/index.js';
8
+
9
+ // 生成唯一 ID
10
+ function generateId(): string {
11
+ return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
12
+ }
13
+
14
+ // 初始状态
15
+ const initialState: ChatState = {
16
+ messages: [],
17
+ isLoading: false,
18
+ isStreaming: false,
19
+ error: null,
20
+ sessionId: generateId()
21
+ };
22
+
23
+ export class ChatStore extends BaseStore<ChatState> {
24
+ private config: ChatConfig = {};
25
+
26
+ constructor() {
27
+ super(initialState);
28
+ }
29
+
30
+ // 设置配置
31
+ setConfig(config: ChatConfig): void {
32
+ this.config = { ...this.config, ...config };
33
+ }
34
+
35
+ // 获取配置
36
+ getConfig(): ChatConfig {
37
+ return this.config;
38
+ }
39
+
40
+ // 添加消息
41
+ addMessage(role: MessageRole, content: string, metadata?: Record<string, unknown>): Message {
42
+ const message: Message = {
43
+ id: generateId(),
44
+ role,
45
+ content,
46
+ timestamp: Date.now(),
47
+ metadata
48
+ };
49
+
50
+ const messages = [...this.state.messages, message];
51
+
52
+ // 限制历史消息数量
53
+ const maxHistory = this.config.maxHistory || 50;
54
+ const trimmedMessages = messages.length > maxHistory
55
+ ? messages.slice(-maxHistory)
56
+ : messages;
57
+
58
+ this.setState({
59
+ messages: trimmedMessages,
60
+ error: null
61
+ });
62
+
63
+ return message;
64
+ }
65
+
66
+ // 更新消息(用于流式输出)
67
+ updateMessage(id: string, content: string): void {
68
+ const messages = this.state.messages.map(msg =>
69
+ msg.id === id ? { ...msg, content } : msg
70
+ );
71
+ this.setState({ messages });
72
+ }
73
+
74
+ // 设置工具调用
75
+ setToolCalls(messageId: string, toolCalls: Message['toolCalls']): void {
76
+ const messages = this.state.messages.map(msg =>
77
+ msg.id === messageId ? { ...msg, toolCalls } : msg
78
+ );
79
+ this.setState({ messages });
80
+ }
81
+
82
+ // 更新工具调用结果
83
+ updateToolCallResult(messageId: string, toolCallId: string, result: unknown): void {
84
+ const messages = this.state.messages.map(msg => {
85
+ if (msg.id === messageId && msg.toolCalls) {
86
+ const toolCalls = msg.toolCalls.map(tc =>
87
+ tc.id === toolCallId ? { ...tc, result, status: 'completed' as const } : tc
88
+ );
89
+ return { ...msg, toolCalls };
90
+ }
91
+ return msg;
92
+ });
93
+ this.setState({ messages });
94
+ }
95
+
96
+ // 设置加载状态
97
+ setLoading(isLoading: boolean): void {
98
+ this.setState({ isLoading });
99
+ }
100
+
101
+ // 设置流式输出状态
102
+ setStreaming(isStreaming: boolean): void {
103
+ this.setState({ isStreaming });
104
+ }
105
+
106
+ // 设置错误
107
+ setError(error: Error | null): void {
108
+ this.setState({ error, isLoading: false, isStreaming: false });
109
+ }
110
+
111
+ // 清空消息
112
+ clearMessages(): void {
113
+ this.setState({ messages: [], error: null });
114
+ }
115
+
116
+ // 删除消息
117
+ deleteMessage(id: string): void {
118
+ const messages = this.state.messages.filter(msg => msg.id !== id);
119
+ this.setState({ messages });
120
+ }
121
+
122
+ // 获取消息历史(用于 API 调用)
123
+ getMessageHistory(): Array<{ role: string; content: string }> {
124
+ return this.state.messages.map(msg => ({
125
+ role: msg.role,
126
+ content: msg.content
127
+ }));
128
+ }
129
+
130
+ // 重置
131
+ reset(): void {
132
+ this.setState(initialState);
133
+ this.config = {};
134
+ }
135
+
136
+ // 导出会话
137
+ exportSession(): string {
138
+ return JSON.stringify({
139
+ sessionId: this.state.sessionId,
140
+ messages: this.state.messages,
141
+ config: this.config,
142
+ exportTime: Date.now()
143
+ });
144
+ }
145
+
146
+ // 导入会话
147
+ importSession(data: string): void {
148
+ try {
149
+ const session = JSON.parse(data);
150
+ this.setState({
151
+ sessionId: session.sessionId || generateId(),
152
+ messages: session.messages || [],
153
+ error: null
154
+ });
155
+ if (session.config) {
156
+ this.config = session.config;
157
+ }
158
+ } catch (error) {
159
+ console.error('Failed to import session:', error);
160
+ this.setError(error as Error);
161
+ }
162
+ }
163
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * @generated-by AI: edenxpzhang
3
+ * @generated-date 2026-05-13
4
+ */
5
+
6
+ import { BaseStore } from './base.js';
7
+ import { ModelConfig } from '../types/index.js';
8
+
9
+ // 初始状态
10
+ const initialConfig: ModelConfig = {
11
+ provider: 'openai',
12
+ model: 'gpt-3.5-turbo',
13
+ params: {
14
+ temperature: 0.7,
15
+ topP: 1,
16
+ maxTokens: 2048
17
+ }
18
+ };
19
+
20
+ export class ModelConfigStore extends BaseStore<ModelConfig> {
21
+ constructor() {
22
+ super({ ...initialConfig });
23
+ }
24
+
25
+ // 设置提供商
26
+ setProvider(provider: ModelConfig['provider']): void {
27
+ this.setState({ provider });
28
+ // 根据提供商自动设置默认端点
29
+ this.setDefaultEndpoint(provider);
30
+ }
31
+
32
+ // 设置模型
33
+ setModel(model: string): void {
34
+ this.setState({ model });
35
+ }
36
+
37
+ // 设置 API 端点
38
+ setApiEndpoint(endpoint: string): void {
39
+ this.setState({ apiEndpoint: endpoint });
40
+ }
41
+
42
+ // 设置 API Key
43
+ setApiKey(apiKey: string): void {
44
+ this.setState({ apiKey });
45
+ }
46
+
47
+ // 设置请求头
48
+ setHeaders(headers: Record<string, string>): void {
49
+ this.setState({ headers: { ...this.state.headers, ...headers } });
50
+ }
51
+
52
+ // 设置参数
53
+ setParams(params: ModelConfig['params']): void {
54
+ this.setState({
55
+ params: { ...this.state.params, ...params }
56
+ });
57
+ }
58
+
59
+ // 根据提供商设置默认端点
60
+ private setDefaultEndpoint(provider: ModelConfig['provider']): void {
61
+ const endpoints: Record<string, string> = {
62
+ openai: 'https://api.openai.com/v1/chat/completions',
63
+ ernie: 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat',
64
+ qwen: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation',
65
+ spark: 'https://spark-api.xf-yun.com/v1/chat/completions',
66
+ hunyuan: 'https://hunyuan.tencentcloudapi.com/'
67
+ };
68
+
69
+ if (provider !== 'custom' && endpoints[provider]) {
70
+ this.setState({ apiEndpoint: endpoints[provider] });
71
+ }
72
+ }
73
+
74
+ // 获取请求头(根据提供商)
75
+ getRequestHeaders(): Record<string, string> {
76
+ const headers: Record<string, string> = {
77
+ 'Content-Type': 'application/json',
78
+ ...this.state.headers
79
+ };
80
+
81
+ // 根据提供商添加认证头
82
+ switch (this.state.provider) {
83
+ case 'openai':
84
+ if (this.state.apiKey) {
85
+ headers['Authorization'] = `Bearer ${this.state.apiKey}`;
86
+ }
87
+ break;
88
+ case 'qwen':
89
+ if (this.state.apiKey) {
90
+ headers['Authorization'] = `Bearer ${this.state.apiKey}`;
91
+ }
92
+ break;
93
+ // 其他提供商的认证方式
94
+ }
95
+
96
+ return headers;
97
+ }
98
+
99
+ // 验证配置
100
+ validate(): { valid: boolean; errors: string[] } {
101
+ const errors: string[] = [];
102
+
103
+ if (!this.state.model) {
104
+ errors.push('Model is required');
105
+ }
106
+
107
+ if (this.state.provider !== 'custom' && !this.state.apiEndpoint) {
108
+ errors.push('API endpoint is required');
109
+ }
110
+
111
+ return {
112
+ valid: errors.length === 0,
113
+ errors
114
+ };
115
+ }
116
+
117
+ // 重置
118
+ reset(): void {
119
+ this.setState({ ...initialConfig });
120
+ }
121
+
122
+ // 导出配置
123
+ exportConfig(): string {
124
+ return JSON.stringify(this.state);
125
+ }
126
+
127
+ // 导入配置
128
+ importConfig(data: string): void {
129
+ try {
130
+ const config = JSON.parse(data);
131
+ this.setState(config);
132
+ } catch (error) {
133
+ console.error('Failed to import config:', error);
134
+ }
135
+ }
136
+ }