📝 前言
在 AI 大模型时代,越来越多的 Web 应用需要集成智能对话功能。然而,前端技术栈的多样性(Vue、React、jQuery 等)给开发者带来了一个难题:如何开发一个一次编写、到处运行的 AI 对话插件?
本文记录了 AI Agent Plugin 项目从诞生到完成的全过程,分享技术选型、架构设计、核心实现以及遇到的挑战与解决方案。
🎯 项目背景与目标
痛点分析
在实际项目中,我们遇到以下问题:
- 技术栈碎片化:公司不同项目使用 Vue 2、Vue 3、React、jQuery 等不同技术栈
- 重复开发成本高:每个技术栈都要单独开发一套 AI 对话组件
- 维护困难:功能更新需要同步修改多个版本
- 样式冲突:插件样式容易被宿主项目覆盖
设计目标
基于以上痛点,我们确定了以下核心目标:
- ✅ 跨框架兼容:一套代码支持 Vue、React、jQuery 等任意前端框架
- ✅ 零依赖:不依赖任何第三方框架,纯原生实现
- ✅ 样式隔离:避免与宿主项目样式冲突
- ✅ TypeScript 支持:提供完整的类型定义
- ✅ 主题定制:支持浅色/深色主题和自定义配色
- ✅ 灵活配置:支持流式/普通响应、四角定位等
🏗️ 技术选型与架构设计
1. 模块化规范:UMD
为了实现跨框架兼容,我们选择了 UMD (Universal Module Definition) 规范:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
(function (root, factory) { if (typeof define === 'function' && define.amd) { define([], factory); } else if (typeof module === 'object' && module.exports) { module.exports = factory(); } else { root.AIAgent = factory(); } }(typeof self !== 'undefined' ? self : this, function () { }));
|
通过 Webpack 的 output.library 配置,我们可以自动生成 UMD 格式的代码:
1 2 3 4 5 6 7 8 9 10 11
| output: { filename: 'ai-agent.js', path: path.resolve(__dirname, 'dist'), library: { name: 'AIAgent', type: 'umd', export: 'default' }, globalObject: 'this' }
|
2. 开发语言:TypeScript
使用 TypeScript 开发带来的优势:
- 类型安全:编译时捕获错误,减少运行时 bug
- 智能提示:IDE 提供完整的代码提示和自动补全
- 可维护性:接口定义即文档,代码意图清晰
核心类型定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| export interface AIAgentOptions { host?: string; secret?: string; stream?: boolean; theme?: 'light' | 'dark'; colors?: { primary?: string; background?: string; text?: string; aiMessageBg?: string; }; position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'; placeholder?: string; title?: string; }
|
3. 样式隔离:命名空间 + CSS 变量
为了避免样式冲突,我们采用了两种策略:
策略一:命名空间隔离
所有 CSS 类名添加 ai-agent- 前缀:
1 2 3
| .ai-agent-panel { } .ai-agent-btn { } .ai-agent-msg { }
|
策略二:CSS 变量动态主题
使用 CSS 变量实现主题定制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| :root { --ai-agent-primary: #4096ff; --ai-agent-primary-hover: #2e80ff; --ai-agent-bg: #ffffff; --ai-agent-text: #333333; }
.ai-agent-theme-light { --ai-agent-bg: #ffffff; --ai-agent-text: #333333; }
.ai-agent-theme-dark { --ai-agent-bg: #1e1e1e; --ai-agent-text: #f0f0f0; }
|
通过 JavaScript 动态注入自定义颜色:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| private injectStyles(): void { const style = document.createElement('style'); let colorVars = ''; if (this.options.colors) { if (this.options.colors.primary) { colorVars += `--ai-agent-primary: ${this.options.colors.primary};\n`; } } style.textContent = ` ${colorVars ? `.ai-agent-theme-${this.options.theme} {\n${colorVars}}` : ''} `; document.head.appendChild(style); }
|
🔧 核心功能实现
1. 插件生命周期管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| class AIAgent { constructor(options: AIAgentOptions) { this.validateOptions(options); this.options = { }; this.endpoints = this.calculateEndpoints(); this.init(); } private init(): void { this.createTriggerButton(); this.createChatPanel(); this.injectStyles(); } public destroy(): void { if (this.buttonEl) this.buttonEl.remove(); if (this.panelEl) this.panelEl.remove(); this.chatHistory = []; } }
|
2. 双模式 API 调用
插件支持普通模式和流式模式两种 API 调用方式:
普通模式(一次性返回)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| private async sendNormalMessage(text: string): Promise<void> { const response = await fetch(this.endpoints.completion, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.options.secret}` }, body: JSON.stringify({ content: text, messages: this.chatHistory }) }); const data: ApiResponse = await response.json(); this.appendMessage(data.data.content, 'ai'); }
|
流式模式(SSE 实时推送)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| private async sendStreamMessage(text: string): Promise<void> { const response = await fetch(this.endpoints.stream, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.options.secret}` }, body: JSON.stringify({ messages: this.chatHistory }) }); const reader = response.body!.getReader(); const decoder = new TextDecoder(); let aiMessage = ''; const messageEl = this.createMessageElement('', 'ai'); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { const data = JSON.parse(line.slice(6)); aiMessage += data.content; messageEl.textContent = aiMessage; } } } }
|
3. 智能端点路由
根据配置自动选择合适的 API 端点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| private calculateEndpoints() { const baseHost = this.options.host || 'http://localhost:8080'; const normalize = (path: string) => baseHost.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, ''); const chatBase = normalize('api/ai/chat'); return { completion: chatBase + '/completion', stream: chatBase + '/stream', streamSimple: chatBase + '/stream-simple', streamConfig: chatBase + '/stream-config', session: normalize('api/ai/session'), platforms: normalize('api/ai/platforms'), fileExtraction: normalize('api/ai/file/extraction') }; }
private sendMessage(text: string): void { if (this.options.stream) { this.sendStreamMessage(text); } else { this.sendNormalMessage(text); } }
|
🎨 主题定制系统
渐进式主题方案
我们设计了三层主题定制方案:
第一层:预设主题
1 2 3
| new AIAgent({ theme: 'light' });
|
第二层:自定义颜色
1 2 3 4 5 6 7 8
| new AIAgent({ theme: 'light', colors: { primary: '#ff6b6b', background: '#f0f0f0', text: '#2c3e50' } });
|
第三层:完整配色方案
1 2 3 4 5 6 7 8 9 10 11 12
| new AIAgent({ colors: { primary: '#ff6b6b', primaryHover: '#ff5252', background: '#f0f0f0', text: '#2c3e50', border: '#e0e0e0', aiMessageBg: '#e8f5e9', userMessageBg: '#ff6b6b', headerBg: '#fafafa' } });
|
CSS 变量的优势
使用 CSS 变量而非直接修改样式的好处:
- 性能更好:浏览器原生支持,无需 JavaScript 逐个修改元素
- 优先级明确:CSS 变量继承机制清晰
- 响应式支持:可以配合媒体查询实现响应式主题
- 易于维护:修改一处,全局生效
🚀 打包与发布
Webpack 多产物输出
为了满足不同场景的需求,我们配置了三种输出格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| module.exports = [ { output: { filename: 'ai-agent.js', library: { name: 'AIAgent', type: 'umd' } } }, { output: { filename: 'ai-agent.min.js', library: { name: 'AIAgent', type: 'umd' } }, optimization: { minimize: true, minimizer: [new TerserPlugin()] } }, { output: { filename: 'ai-agent.esm.js', library: { type: 'module' } }, experiments: { outputModule: true } } ];
|
TypeScript 类型声明
使用 ts-loader 自动生成类型声明文件:
1 2 3 4 5 6 7 8 9
| module: { rules: [ { test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/ } ] }
|
生成的类型文件结构:
dist/
├── ai-agent.js # UMD 格式(未压缩)
├── ai-agent.min.js # UMD 格式(压缩)
├── ai-agent.esm.js # ESM 格式
└── types/
├── ai-agent.d.ts # 主入口类型声明
└── types/
└── index.d.ts # 类型定义
NPM 包配置
1 2 3 4 5 6 7 8 9 10 11 12
| { "name": "ai-agent-plugin", "version": "0.0.1", "main": "dist/ai-agent.js", "module": "dist/ai-agent.esm.js", "types": "dist/types/ai-agent.d.ts", "files": [ "dist/", "README.md", "LICENSE" ] }
|
🎯 跨框架使用实践
1. jQuery 项目
1 2 3 4 5 6 7 8 9 10 11 12
| <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="path/to/ai-agent.min.js"></script>
<script> $(document).ready(function() { const aiAgent = new AIAgent({ host: 'http://localhost:8080', secret: 'your-api-key', theme: 'light' }); }); </script>
|
2. React 项目
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import React, { useEffect } from 'react'; import AIAgent from 'ai-agent-plugin';
function App() { useEffect(() => { const aiAgent = new AIAgent({ host: 'http://localhost:8080', secret: process.env.REACT_APP_AI_SECRET, theme: 'dark', stream: true }); return () => aiAgent.destroy(); }, []);
return <div>My App</div>; }
|
3. Vue 3 项目
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <template> <div>My App</div> </template>
<script setup> import { onMounted, onUnmounted } from 'vue'; import AIAgent from 'ai-agent-plugin';
let aiAgent = null;
onMounted(() => { aiAgent = new AIAgent({ host: 'http://localhost:8080', secret: import.meta.env.VITE_AI_SECRET, position: 'bottom-left', colors: { primary: '#42b883' // Vue 绿 } }); });
onUnmounted(() => { aiAgent?.destroy(); }); </script>
|
🐛 踩坑与解决方案
问题 1:TypeScript 类型不兼容
现象:使用 Required<AIAgentOptions> 导致可选属性变必选
1 2 3 4 5 6 7 8 9
| private options: Required<AIAgentOptions>;
this.options = { host: '...', secret: '...', };
|
解决方案:直接使用原接口类型
1 2
| private options: AIAgentOptions;
|
问题 2:端口占用导致开发服务器无法启动
现象:Error: listen EADDRINUSE: address already in use :::9000
解决方案:PowerShell 脚本清理占用端口
1 2 3 4
| Get-NetTCPConnection -LocalPort 9000 -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess | ForEach-Object { Stop-Process -Id $_ -Force }
|
问题 3:CSS 变量在 IE 中不生效
现象:IE 11 不支持 CSS 变量
解决方案:提供回退方案
1 2 3 4
| .ai-agent-btn { background: #4096ff; background: var(--ai-agent-primary); }
|
或者使用 PostCSS 插件自动生成回退:
1 2 3 4 5 6 7 8
| module.exports = { plugins: [ require('postcss-custom-properties')({ preserve: false }) ] };
|
问题 4:流式响应解析错误
现象:SSE 数据格式不规范导致解析失败
解决方案:增强容错处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| for (const line of lines) { if (!line.trim() || !line.startsWith('data: ')) continue; try { const jsonStr = line.slice(6).trim(); if (jsonStr === '[DONE]') break; const data = JSON.parse(jsonStr); if (data.content) { aiMessage += data.content; } } catch (err) { console.warn('解析 SSE 数据失败:', line, err); } }
|
📊 性能优化
1. 代码分割与按需加载
虽然插件本身较小(~20KB),但仍可优化:
1 2 3 4 5
| private async renderMarkdown(text: string): Promise<string> { const { marked } = await import('marked'); return marked(text); }
|
2. 防抖与节流
输入框添加防抖,避免频繁触发:
1 2 3 4 5 6 7 8 9 10 11
| private handleInput = debounce((text: string) => { }, 300);
function debounce(fn: Function, delay: number) { let timer: number; return function(...args: any[]) { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; }
|
3. 虚拟滚动(未来规划)
对话消息过多时,使用虚拟滚动:
1 2 3 4 5 6 7 8 9 10
| private renderVisibleMessages() { const scrollTop = this.messagesEl.scrollTop; const visibleHeight = this.messagesEl.clientHeight; const startIndex = Math.floor(scrollTop / MESSAGE_HEIGHT); const endIndex = Math.ceil((scrollTop + visibleHeight) / MESSAGE_HEIGHT); return this.chatHistory.slice(startIndex, endIndex); }
|
🔮 未来规划
短期目标(v0.1.0)
- [ ] 支持 Markdown 渲染
- [ ] 代码高亮显示
- [ ] 消息历史持久化(LocalStorage)
- [ ] 消息重发/编辑功能
- [ ] 多语言支持(i18n)
中期目标(v0.2.0)
- [ ] 语音输入/输出
- [ ] 文件上传(图片、文档)
- [ ] 多会话管理
- [ ] 自定义快捷指令
- [ ] WebSocket 支持
长期目标(v1.0.0)
- [ ] 插件市场(第三方扩展)
- [ ] AI 能力扩展(绘画、搜索、代码执行)
- [ ] 团队协作功能
- [ ] 移动端适配(React Native)
- [ ] Electron 桌面应用
💡 经验总结
技术层面
- 选择合适的模块化规范:UMD 确实能实现跨框架兼容
- TypeScript 提升开发体验:类型安全减少 80% 的运行时错误
- CSS 变量是主题定制的最佳实践:灵活、高效、易维护
- 流式响应提升用户体验:实时反馈比加载动画更友好
工程层面
- 文档优先:README 写得好,issue 减少一半
- 示例丰富:提供 jQuery/React/Vue 示例,降低使用门槛
- 语义化版本:严格遵循 SemVer,避免破坏性更新
- 自动化测试(规划中):保证多框架环境下的稳定性
产品层面
- 保持简单:不要过度设计,先满足核心需求
- 渐进增强:功能分层,让用户自由选择
- 用户反馈驱动:根据 issue 和 PR 优化功能
- 社区运营:开源项目需要持续的社区互动
🙏 致谢
感谢以下开源项目的启发:
感谢所有给项目提 issue 和 PR 的开发者!
📚 参考资料
结语
从零打造一个跨框架通用插件是一次有趣的技术实践,它让我们深入理解了:
- 模块化规范的本质
- TypeScript 的工程化价值
- CSS 架构的设计思想
- 前端构建工具的工作原理
希望这篇文章能给正在开发跨框架组件的开发者一些启发。如果你有任何问题或建议,欢迎在 GitHub 上提 issue 讨论!
项目地址:https://github.com/hujinbin/ai-agent
NPM 包:npm install ai-agent-plugin
开源协议:MIT License
觉得有帮助?给个 ⭐ Star 吧!