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,206 @@
1
+ /**
2
+ * @generated-by AI: edenxpzhang
3
+ * @generated-date 2026-05-13
4
+ */
5
+
6
+ import { Marked } from 'marked';
7
+ import { markedHighlight } from 'marked-highlight';
8
+ import hljs from 'highlight.js';
9
+
10
+ // 声明全局 __copyCode 方法
11
+ declare global {
12
+ interface Window {
13
+ __copyCode(button: HTMLElement): void;
14
+ }
15
+ }
16
+
17
+ // 创建 marked 实例
18
+ const marked = new Marked(
19
+ markedHighlight({
20
+ langPrefix: 'hljs language-',
21
+ highlight(code, lang) {
22
+ if (lang && hljs.getLanguage(lang)) {
23
+ return hljs.highlight(code, { language: lang }).value;
24
+ }
25
+ return hljs.highlightAuto(code).value;
26
+ }
27
+ })
28
+ );
29
+
30
+ // 配置 marked
31
+ marked.setOptions({
32
+ breaks: true,
33
+ gfm: true
34
+ });
35
+
36
+ /**
37
+ * 自定义 renderer,扩展代码块渲染
38
+ */
39
+ function createRenderer() {
40
+ const renderer = new marked.Renderer();
41
+
42
+ // 自定义代码块渲染
43
+ renderer.code = function(code: string, infostring: string | undefined, escaped: boolean) {
44
+ const language = infostring || '';
45
+ const highlighted = language && hljs.getLanguage(language)
46
+ ? hljs.highlight(code, { language }).value
47
+ : hljs.highlightAuto(code).value;
48
+
49
+ return `
50
+ <div class="markdown-code-block" data-language="${language}">
51
+ <div class="code-block-header">
52
+ <span class="code-language">${language || 'plaintext'}</span>
53
+ <button class="code-copy-btn" onclick="window.__copyCode(this)">复制</button>
54
+ </div>
55
+ <pre><code class="hljs language-${language}">${highlighted}</code></pre>
56
+ </div>
57
+ `;
58
+ };
59
+
60
+ // 自定义表格渲染
61
+ renderer.table = function(header: string, body: string) {
62
+ return `
63
+ <div class="markdown-table-wrapper">
64
+ <table>
65
+ <thead>${header}</thead>
66
+ <tbody>${body}</tbody>
67
+ </table>
68
+ </div>
69
+ `;
70
+ };
71
+
72
+ return renderer;
73
+ }
74
+
75
+ // 设置自定义 renderer
76
+ marked.use({ renderer: createRenderer() });
77
+
78
+ /**
79
+ * 解析 Markdown 为 HTML
80
+ * @param content Markdown 字符串
81
+ * @returns 解析后的 HTML 字符串
82
+ */
83
+ export function parseMarkdown(content: string): string {
84
+ if (!content) {
85
+ return '';
86
+ }
87
+
88
+ try {
89
+ return marked.parse(content) as string;
90
+ } catch (error) {
91
+ console.error('Markdown parse error:', error);
92
+ // 降级:转义 HTML 并返回纯文本
93
+ return escapeHtml(content);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * 检测内容是否包含 Markdown 语法
99
+ * @param content 待检测内容
100
+ * @returns 是否包含 Markdown
101
+ */
102
+ export function detectMarkdown(content: string): boolean {
103
+ if (!content) {
104
+ return false;
105
+ }
106
+
107
+ // 常见的 Markdown 语法正则
108
+ const markdownPatterns = [
109
+ /^#{1,6}\s+.+/m, // 标题
110
+ /`{3}[\s\S]*?`{3}/, // 代码块
111
+ /`[^`]+`/, // 行内代码
112
+ /\*\*[^*]+\*\*/, // 粗体
113
+ /\*[^*]+\*/, // 斜体
114
+ /\[.+?\]\(.+?\)/, // 链接
115
+ /!\[.+?\]\(.+?\)/, // 图片
116
+ /^\s*[-*+]\s+.+/m, // 无序列表
117
+ /^\s*\d+\.\s+.+/m, // 有序列表
118
+ /^\s*>.+/m, // 引用
119
+ /\|(.+)\|/, // 表格
120
+ /^---$/m // 分隔线
121
+ ];
122
+
123
+ return markdownPatterns.some(pattern => pattern.test(content));
124
+ }
125
+
126
+ /**
127
+ * 转义 HTML 特殊字符
128
+ */
129
+ function escapeHtml(text: string): string {
130
+ const map: Record<string, string> = {
131
+ '&': '&amp;',
132
+ '<': '&lt;',
133
+ '>': '&gt;',
134
+ '"': '&quot;',
135
+ "'": '&#039;'
136
+ };
137
+
138
+ return text.replace(/[&<>"']/g, char => map[char]);
139
+ }
140
+
141
+ /**
142
+ * 注册全局代码复制函数
143
+ * 需要在浏览器环境中调用
144
+ */
145
+ export function registerCodeCopyHandler(): void {
146
+ if (typeof window !== 'undefined') {
147
+ window.__copyCode = function(button: HTMLElement) {
148
+ const codeBlock = button.closest('.markdown-code-block');
149
+ if (!codeBlock) return;
150
+
151
+ const code = codeBlock.querySelector('code');
152
+ if (!code) return;
153
+
154
+ const text = code.textContent || '';
155
+
156
+ navigator.clipboard.writeText(text).then(() => {
157
+ const originalText = button.textContent;
158
+ button.textContent = '已复制';
159
+ button.classList.add('copied');
160
+
161
+ setTimeout(() => {
162
+ button.textContent = originalText;
163
+ button.classList.remove('copied');
164
+ }, 2000);
165
+ }).catch(err => {
166
+ console.error('Copy failed:', err);
167
+ });
168
+ };
169
+ }
170
+ }
171
+
172
+ /**
173
+ * 渲染 Markdown 到指定容器
174
+ * @param content Markdown 内容
175
+ * @param container 目标容器元素
176
+ * @returns 渲染后的 HTML 字符串
177
+ */
178
+ export function renderMarkdownToContainer(content: string, container: HTMLElement): string {
179
+ const html = parseMarkdown(content);
180
+ container.innerHTML = html;
181
+
182
+ // 为代码块添加复制功能
183
+ container.querySelectorAll('.code-copy-btn').forEach(btn => {
184
+ btn.addEventListener('click', function(this: HTMLButtonElement) {
185
+ const codeBlock = this.closest('.markdown-code-block');
186
+ if (!codeBlock) return;
187
+
188
+ const code = codeBlock.querySelector('code');
189
+ if (!code) return;
190
+
191
+ const text = code.textContent || '';
192
+ navigator.clipboard.writeText(text).then(() => {
193
+ const originalText = this.textContent;
194
+ this.textContent = '已复制';
195
+ this.classList.add('copied');
196
+
197
+ setTimeout(() => {
198
+ this.textContent = originalText;
199
+ this.classList.remove('copied');
200
+ }, 2000);
201
+ });
202
+ });
203
+ });
204
+
205
+ return html;
206
+ }
@@ -0,0 +1,276 @@
1
+ /**
2
+ * @generated-by AI: edenxpzhang
3
+ * @generated-date 2026-05-13
4
+ */
5
+
6
+ /**
7
+ * Mermaid 图表类型
8
+ */
9
+ export type MermaidChartType =
10
+ | 'graph' // 流程图
11
+ | 'flowchart' // 流图(新语法)
12
+ | 'sequence' // 时序图
13
+ | 'class' // 类图
14
+ | 'state' // 状态图
15
+ | 'er' // ER 图
16
+ | 'journey' // 用户旅程图
17
+ | 'gantt' // 甘特图
18
+ | 'pie' // 饼图
19
+ | 'gitgraph' // Git 图
20
+ | 'mindmap' // 思维导图
21
+ | 'timeline' // 时间线
22
+ | 'unknown'; // 未知类型
23
+
24
+ /**
25
+ * Mermaid 检测结果的接口
26
+ */
27
+ export interface MermaidDetectionResult {
28
+ isMermaid: boolean;
29
+ chartType: MermaidChartType;
30
+ code: string;
31
+ }
32
+
33
+ /**
34
+ * 检测代码块是否为 Mermaid 图表
35
+ * @param code 待检测的代码字符串
36
+ * @returns 是否为 Mermaid 图表
37
+ */
38
+ export function detectMermaid(code: string): boolean {
39
+ if (!code || typeof code !== 'string') {
40
+ return false;
41
+ }
42
+
43
+ const trimmed = code.trim();
44
+
45
+ // Mermaid 代码块通常以这些关键字开头
46
+ const mermaidKeywords = [
47
+ 'graph', // 流程图(LR, RL, TB, BT 方向)
48
+ 'flowchart', // 新流程图语法
49
+ 'sequenceDiagram', // 时序图
50
+ 'classDiagram', // 类图
51
+ 'stateDiagram', // 状态图
52
+ 'erDiagram', // ER 图
53
+ 'journey', // 用户旅程图
54
+ 'gantt', // 甘特图
55
+ 'pie', // 饼图
56
+ 'gitGraph', // Git 图
57
+ 'mindmap', // 思维导图
58
+ 'timeline', // 时间线
59
+ 'quadrantChart' // 象限图
60
+ ];
61
+
62
+ // 检查是否以 Mermaid 关键字开头
63
+ for (const keyword of mermaidKeywords) {
64
+ if (trimmed.startsWith(keyword)) {
65
+ return true;
66
+ }
67
+ }
68
+
69
+ // 检查 graph 方向简写(如 graph TD, graph LR)
70
+ const graphDirectionRegex = /^(graph|flowchart)\s+(TB|TD|BT|RL|LR)/i;
71
+ if (graphDirectionRegex.test(trimmed)) {
72
+ return true;
73
+ }
74
+
75
+ return false;
76
+ }
77
+
78
+ /**
79
+ * 检测 Mermaid 图表类型
80
+ * @param code Mermaid 代码
81
+ * @returns 图表类型
82
+ */
83
+ export function detectMermaidChartType(code: string): MermaidChartType {
84
+ if (!code || typeof code !== 'string') {
85
+ return 'unknown';
86
+ }
87
+
88
+ const trimmed = code.trim();
89
+
90
+ if (/^(graph|flowchart)/i.test(trimmed)) {
91
+ return 'flowchart';
92
+ }
93
+
94
+ if (/^sequenceDiagram/i.test(trimmed)) {
95
+ return 'sequence';
96
+ }
97
+
98
+ if (/^classDiagram/i.test(trimmed)) {
99
+ return 'class';
100
+ }
101
+
102
+ if (/^stateDiagram/i.test(trimmed)) {
103
+ return 'state';
104
+ }
105
+
106
+ if (/^erDiagram/i.test(trimmed)) {
107
+ return 'er';
108
+ }
109
+
110
+ if (/^journey/i.test(trimmed)) {
111
+ return 'journey';
112
+ }
113
+
114
+ if (/^gantt/i.test(trimmed)) {
115
+ return 'gantt';
116
+ }
117
+
118
+ if (/^pie/i.test(trimmed)) {
119
+ return 'pie';
120
+ }
121
+
122
+ if (/^gitGraph/i.test(trimmed)) {
123
+ return 'gitgraph';
124
+ }
125
+
126
+ if (/^mindmap/i.test(trimmed)) {
127
+ return 'mindmap';
128
+ }
129
+
130
+ if (/^timeline/i.test(trimmed)) {
131
+ return 'timeline';
132
+ }
133
+
134
+ return 'unknown';
135
+ }
136
+
137
+ /**
138
+ * 从 Markdown 内容中提取 Mermaid 代码块
139
+ * @param content Markdown 内容
140
+ * @returns 提取的 Mermaid 代码数组
141
+ */
142
+ export function extractMermaidFromMarkdown(content: string): string[] {
143
+ if (!content) {
144
+ return [];
145
+ }
146
+
147
+ const mermaidBlocks: string[] = [];
148
+
149
+ // 匹配 ```mermaid ... ``` 代码块
150
+ const mermaidRegex = /```mermaid\s*([\s\S]*?)```/gi;
151
+ let match: RegExpExecArray | null;
152
+
153
+ while ((match = mermaidRegex.exec(content)) !== null) {
154
+ if (match[1]) {
155
+ mermaidBlocks.push(match[1].trim());
156
+ }
157
+ }
158
+
159
+ return mermaidBlocks;
160
+ }
161
+
162
+ /**
163
+ * 渲染 Mermaid 图表到指定容器
164
+ * @param code Mermaid 代码
165
+ * @param container 目标容器元素
166
+ * @param options 可选配置
167
+ */
168
+ export function renderMermaid(
169
+ code: string,
170
+ container: HTMLElement,
171
+ options?: {
172
+ theme?: 'default' | 'dark' | 'forest' | 'neutral';
173
+ backgroundColor?: string;
174
+ scale?: number;
175
+ }
176
+ ): void {
177
+ if (!code || !container) {
178
+ console.error('renderMermaid: code or container is missing');
179
+ return;
180
+ }
181
+
182
+ // 检查 mermaid 是否可用
183
+ if (typeof window === 'undefined') {
184
+ console.error('renderMermaid: only works in browser environment');
185
+ return;
186
+ }
187
+
188
+ // 动态导入 mermaid(避免 SSR 问题)
189
+ import('mermaid')
190
+ .then((mermaid) => {
191
+ // 初始化 mermaid
192
+ mermaid.default.initialize({
193
+ startOnLoad: false,
194
+ theme: options?.theme || 'default',
195
+ securityLevel: 'loose',
196
+ flowchart: { useMaxWidth: true, htmlLabels: true },
197
+ ...options
198
+ });
199
+
200
+ // 生成唯一 ID
201
+ const id = `mermaid-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
202
+
203
+ // 创建渲染容器
204
+ const wrapper = document.createElement('div');
205
+ wrapper.className = 'mermaid-chart-wrapper';
206
+ wrapper.style.overflow = 'auto';
207
+
208
+ const diagramDiv = document.createElement('div');
209
+ diagramDiv.className = 'mermaid-chart';
210
+ diagramDiv.id = id;
211
+
212
+ // 设置 mermaid 代码
213
+ diagramDiv.textContent = code;
214
+
215
+ wrapper.appendChild(diagramDiv);
216
+ container.appendChild(wrapper);
217
+
218
+ // 渲染
219
+ mermaid.default.run({
220
+ querySelector: `#${id}`
221
+ }).catch((err: Error) => {
222
+ console.error('Mermaid render error:', err);
223
+ diagramDiv.innerHTML = `<pre style="color:red;">Mermaid 渲染失败: ${err.message}</pre>`;
224
+ });
225
+ })
226
+ .catch((err) => {
227
+ console.error('Failed to load mermaid:', err);
228
+ container.innerHTML = `<pre style="color:red;">无法加载 Mermaid 库</pre>`;
229
+ });
230
+ }
231
+
232
+ /**
233
+ * 批量渲染容器中的所有 Mermaid 代码块
234
+ * @param container 容器元素
235
+ */
236
+ export function renderAllMermaidInContainer(container: HTMLElement): void {
237
+ if (!container) {
238
+ return;
239
+ }
240
+
241
+ // 查找所有 class 包含 language-mermaid 的 code 元素
242
+ const mermaidElements = container.querySelectorAll('code.language-mermaid, code.language-flow');
243
+
244
+ mermaidElements.forEach((el) => {
245
+ const code = el.textContent || '';
246
+ const parent = el.closest('pre');
247
+
248
+ if (parent) {
249
+ const wrapper = document.createElement('div');
250
+ wrapper.className = 'mermaid-chart-wrapper';
251
+
252
+ parent.parentNode?.replaceChild(wrapper, parent);
253
+ renderMermaid(code, wrapper);
254
+ }
255
+ });
256
+ }
257
+
258
+ /**
259
+ * 验证 Mermaid 代码语法
260
+ * @param code Mermaid 代码
261
+ * @returns 是否有效
262
+ */
263
+ export async function validateMermaid(code: string): Promise<boolean> {
264
+ if (!code) {
265
+ return false;
266
+ }
267
+
268
+ try {
269
+ const mermaid = await import('mermaid');
270
+ await mermaid.default.parse(code);
271
+ return true;
272
+ } catch (error) {
273
+ console.error('Mermaid validation error:', error);
274
+ return false;
275
+ }
276
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * @generated-by AI: edenxpzhang
3
+ * @generated-date 2026-05-13
4
+ */
5
+
6
+ import { MessagePlugin, Message, RenderContext } from '../types/index.js';
7
+
8
+ /**
9
+ * 插件管理器
10
+ * 负责管理所有消息插件,包括注册、注销、匹配和渲染
11
+ */
12
+ export class PluginManager {
13
+ private plugins: Map<string, MessagePlugin> = new Map();
14
+ private renderOrder: string[] = []; // 渲染顺序(按优先级)
15
+
16
+ /**
17
+ * 注册插件
18
+ * @param plugin 插件实例
19
+ */
20
+ register(plugin: MessagePlugin): void {
21
+ if (!plugin || !plugin.type) {
22
+ console.error('PluginManager: Cannot register invalid plugin', plugin);
23
+ return;
24
+ }
25
+
26
+ if (this.plugins.has(plugin.type)) {
27
+ console.warn(`PluginManager: Plugin "${plugin.type}" is already registered, overwriting`);
28
+ }
29
+
30
+ this.plugins.set(plugin.type, plugin);
31
+ this.updateRenderOrder();
32
+ }
33
+
34
+ /**
35
+ * 注销插件
36
+ * @param type 插件类型标识
37
+ */
38
+ unregister(type: string): void {
39
+ if (this.plugins.has(type)) {
40
+ this.plugins.delete(type);
41
+ this.updateRenderOrder();
42
+ }
43
+ }
44
+
45
+ /**
46
+ * 获取指定类型的插件
47
+ * @param type 插件类型标识
48
+ * @returns 插件实例或 undefined
49
+ */
50
+ getPlugin(type: string): MessagePlugin | undefined {
51
+ return this.plugins.get(type);
52
+ }
53
+
54
+ /**
55
+ * 获取所有已注册的插件
56
+ * @returns 插件数组
57
+ */
58
+ getAllPlugins(): MessagePlugin[] {
59
+ return Array.from(this.plugins.values());
60
+ }
61
+
62
+ /**
63
+ * 根据内容匹配最合适的插件(按优先级排序)
64
+ * @param content 消息内容
65
+ * @returns 匹配的插件或 null
66
+ */
67
+ matchPlugin(content: string): MessagePlugin | null {
68
+ if (!content || typeof content !== 'string') {
69
+ return null;
70
+ }
71
+
72
+ const matchedPlugins: Array<{ plugin: MessagePlugin; priority: number }> = [];
73
+
74
+ for (const plugin of this.plugins.values()) {
75
+ if (plugin.match && typeof plugin.match === 'function') {
76
+ try {
77
+ const isMatch = plugin.match(content);
78
+ if (isMatch) {
79
+ matchedPlugins.push({
80
+ plugin,
81
+ priority: plugin.priority || 0
82
+ });
83
+ }
84
+ } catch (error) {
85
+ console.error(`PluginManager: Error in plugin "${plugin.type}" match function:`, error);
86
+ }
87
+ }
88
+ }
89
+
90
+ // 按优先级排序(数字越大优先级越高)
91
+ matchedPlugins.sort((a, b) => b.priority - a.priority);
92
+
93
+ return matchedPlugins.length > 0 ? matchedPlugins[0].plugin : null;
94
+ }
95
+
96
+ /**
97
+ * 匹配所有符合条件的插件(按优先级排序)
98
+ * @param content 消息内容
99
+ * @returns 所有匹配的插件数组
100
+ */
101
+ matchAllPlugins(content: string): MessagePlugin[] {
102
+ if (!content || typeof content !== 'string') {
103
+ return [];
104
+ }
105
+
106
+ const matchedPlugins: Array<{ plugin: MessagePlugin; priority: number }> = [];
107
+
108
+ for (const plugin of this.plugins.values()) {
109
+ if (plugin.match && typeof plugin.match === 'function') {
110
+ try {
111
+ const isMatch = plugin.match(content);
112
+ if (isMatch) {
113
+ matchedPlugins.push({
114
+ plugin,
115
+ priority: plugin.priority || 0
116
+ });
117
+ }
118
+ } catch (error) {
119
+ console.error(`PluginManager: Error in plugin "${plugin.type}" match function:`, error);
120
+ }
121
+ }
122
+ }
123
+
124
+ // 按优先级排序
125
+ matchedPlugins.sort((a, b) => b.priority - a.priority);
126
+
127
+ return matchedPlugins.map(item => item.plugin);
128
+ }
129
+
130
+ /**
131
+ * 使用匹配的插件渲染内容
132
+ * @param content 消息内容
133
+ * @param context 渲染上下文
134
+ * @returns 渲染结果(HTMLElement 或 HTML 字符串)
135
+ */
136
+ renderWithPlugin(content: string, context: RenderContext): HTMLElement | string {
137
+ const plugin = this.matchPlugin(content);
138
+
139
+ if (plugin && plugin.render) {
140
+ try {
141
+ return plugin.render(content, context);
142
+ } catch (error) {
143
+ console.error(`PluginManager: Error in plugin "${plugin.type}" render function:`, error);
144
+ return content; // 降级返回原始内容
145
+ }
146
+ }
147
+
148
+ // 没有匹配的插件,返回原始内容
149
+ return content;
150
+ }
151
+
152
+ /**
153
+ * 渲染所有匹配的插件(用于一个消息需要多个插件处理的情况)
154
+ * @param content 消息内容
155
+ * @param context 渲染上下文
156
+ * @returns 渲染结果数组
157
+ */
158
+ renderWithAllPlugins(content: string, context: RenderContext): Array<{ plugin: MessagePlugin; result: HTMLElement | string }> {
159
+ const plugins = this.matchAllPlugins(content);
160
+ const results: Array<{ plugin: MessagePlugin; result: HTMLElement | string }> = [];
161
+
162
+ for (const plugin of plugins) {
163
+ if (plugin.render) {
164
+ try {
165
+ const result = plugin.render(content, context);
166
+ results.push({ plugin, result });
167
+ } catch (error) {
168
+ console.error(`PluginManager: Error in plugin "${plugin.type}" render function:`, error);
169
+ }
170
+ }
171
+ }
172
+
173
+ return results;
174
+ }
175
+
176
+ /**
177
+ * 清除所有插件
178
+ */
179
+ clear(): void {
180
+ this.plugins.clear();
181
+ this.renderOrder = [];
182
+ }
183
+
184
+ /**
185
+ * 获取已注册插件的数量
186
+ */
187
+ get size(): number {
188
+ return this.plugins.size;
189
+ }
190
+
191
+ /**
192
+ * 检查是否注册了指定类型的插件
193
+ * @param type 插件类型标识
194
+ */
195
+ hasPlugin(type: string): boolean {
196
+ return this.plugins.has(type);
197
+ }
198
+
199
+ /**
200
+ * 更新渲染顺序(按优先级排序)
201
+ */
202
+ private updateRenderOrder(): void {
203
+ const sorted = Array.from(this.plugins.entries())
204
+ .sort((a, b) => {
205
+ const priorityA = a[1].priority || 0;
206
+ const priorityB = b[1].priority || 0;
207
+ return priorityB - priorityA; // 降序
208
+ })
209
+ .map(([type]) => type);
210
+
211
+ this.renderOrder = sorted;
212
+ }
213
+ }
214
+
215
+ /**
216
+ * 创建插件管理器的单例
217
+ */
218
+ let defaultPluginManager: PluginManager | null = null;
219
+
220
+ export function getDefaultPluginManager(): PluginManager {
221
+ if (!defaultPluginManager) {
222
+ defaultPluginManager = new PluginManager();
223
+ }
224
+ return defaultPluginManager;
225
+ }
226
+
227
+ export function resetDefaultPluginManager(): void {
228
+ if (defaultPluginManager) {
229
+ defaultPluginManager.clear();
230
+ }
231
+ defaultPluginManager = new PluginManager();
232
+ }