var ChatbotSDK = (function () { 'use strict'; const PREFIX = '[ChatbotSDK]'; let debugEnabled = true; /** 设置是否开启调试日志 */ function setDebug(enabled) { debugEnabled = enabled; } const logger = { /** 普通信息日志 */ info(msg, data) { if (debugEnabled) { console.log(PREFIX, msg, data !== undefined ? data : ''); } }, /** 警告日志 */ warn(msg, data) { if (debugEnabled) { console.warn(PREFIX, msg, data !== undefined ? data : ''); } }, /** 错误日志(始终输出,不受 debug 开关控制) */ error(msg, data) { console.error(PREFIX, msg, data !== undefined ? data : ''); }, }; /** 默认悬浮按钮 SVG 图标(客服对话气泡) */ const DEFAULT_LAUNCHER_ICON = ` `; /** * 解析并校验用户传入的配置,填充默认值 */ function parseConfig(raw) { var _a, _b, _c, _d, _e, _f; // 校验必传参数:integrateId if (!raw.integrateId || typeof raw.integrateId !== 'string' || raw.integrateId.trim() === '') { logger.error('integrateId 是必传参数,请检查 init() 调用。示例:ChatbotSDK.init({ integrateId: "my-app", requestDomain: "https://api.example.com" })'); return null; } // 校验必传参数:requestDomain if (!raw.requestDomain || typeof raw.requestDomain !== 'string' || raw.requestDomain.trim() === '') { logger.error('requestDomain 是必传参数,请检查 init() 调用。示例:ChatbotSDK.init({ integrateId: "my-app", requestDomain: "https://api.example.com" })'); return null; } // 校验 requestDomain 是否为合法 URL 格式 try { new URL(raw.requestDomain); } catch (_g) { logger.error(`requestDomain 不是合法的 URL 格式:${raw.requestDomain}。请提供完整的域名,如 https://api.example.com`); return null; } // 填充默认值 const config = { integrateId: raw.integrateId.trim(), requestDomain: raw.requestDomain.replace(/\/+$/, ''), // 去掉末尾斜杠 userId: raw.userId, roleId: raw.roleId, categoryId: raw.categoryId, showCategorySwitch: (_a = raw.showCategorySwitch) !== null && _a !== void 0 ? _a : false, title: raw.title || 'AI 智能助手', width: (_b = raw.width) !== null && _b !== void 0 ? _b : 380, position: raw.position === 'left-bottom' ? 'left-bottom' : 'right-bottom', primaryColor: raw.primaryColor || '#4F46E5', launcherIcon: raw.launcherIcon || DEFAULT_LAUNCHER_ICON, showClear: (_c = raw.showClear) !== null && _c !== void 0 ? _c : true, showAdminPanel: (_d = raw.showAdminPanel) !== null && _d !== void 0 ? _d : false, streaming: (_e = raw.streaming) !== null && _e !== void 0 ? _e : true, locale: raw.locale || 'zh-CN', debug: (_f = raw.debug) !== null && _f !== void 0 ? _f : true, }; logger.info(`配置解析完成 integrateId=${config.integrateId} requestDomain=${config.requestDomain}`); return config; } /** 请求超时时间(毫秒) */ const REQUEST_TIMEOUT = 30000; let currentConfig = null; /** 设置当前配置 */ function setApiConfig(config) { currentConfig = config; } /** 构建完整请求 URL,自动防御双斜杠 */ function buildUrl(path) { if (!currentConfig) { throw new Error('API 配置未初始化'); } const domain = currentConfig.requestDomain.replace(/\/+$/, ''); const cleanPath = path.startsWith('/') ? path : `/${path}`; return `${domain}${cleanPath}`; } /** 构建同步对话请求 URL */ function buildChatUrl(message) { const params = new URLSearchParams(); params.set('message', message); params.set('chatId', currentConfig.integrateId); if (currentConfig.userId) { params.set('accountId', currentConfig.userId); } if (currentConfig.roleId) { params.set('roleId', String(currentConfig.roleId)); } if (currentConfig.categoryId) { params.set('categoryId', String(currentConfig.categoryId)); } return buildUrl(`/ai/assistant_app/chat/sync?${params.toString()}`); } /** 构建 SSE 流式请求 URL */ function buildChatSSEUrl(message) { const params = new URLSearchParams(); params.set('message', message); params.set('chatId', currentConfig.integrateId); if (currentConfig.userId) { params.set('accountId', currentConfig.userId); } if (currentConfig.roleId) { params.set('roleId', String(currentConfig.roleId)); } if (currentConfig.categoryId) { params.set('categoryId', String(currentConfig.categoryId)); } return buildUrl(`/ai/assistant_app/chat/sse?${params.toString()}`); } /** 带超时的 fetch 封装 */ async function safeFetch(url, options = {}, timeout = REQUEST_TIMEOUT) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, Object.assign(Object.assign({}, options), { signal: controller.signal, mode: 'cors', credentials: 'include' })); return response; } catch (err) { if (err instanceof DOMException && err.name === 'AbortError') { throw new CskError('请求超时,请稍后重试', 'timeout'); } if (err instanceof TypeError && err.message.includes('Failed to fetch')) { throw new CskError('跨域请求被拦截,请联系管理员将当前域名加入 API 白名单', 'cors'); } throw new CskError('网络连接失败,请检查网络', 'network'); } finally { clearTimeout(timer); } } /** 自定义错误类型 */ class CskError extends Error { constructor(message, type) { super(message); this.name = 'CskError'; this.type = type; } } /** 根据 HTTP 状态码返回对应的中文错误消息 */ function getHttpErrorMessage(status) { switch (status) { case 401: return '鉴权失败,请联系管理员'; case 403: return '无访问权限,请联系管理员配置'; case 404: return '请求的资源不存在'; case 429: return '请求过于频繁,请稍后重试'; case 500: return '服务器异常,请稍后重试'; case 502: case 503: return '服务暂不可用,请稍后重试'; default: return `请求失败(状态码 ${status})`; } } /** * 同步对话请求 */ async function chatRequest(message) { const url = buildChatUrl(message); const startTime = Date.now(); logger.info(`发送消息 integrateId=${currentConfig.integrateId} length=${message.length}`); try { const response = await safeFetch(url); if (!response.ok) { const errorMsg = getHttpErrorMessage(response.status); logger.error(`请求失败 integrateId=${currentConfig.integrateId} status=${response.status} message=${errorMsg}`); throw new CskError(errorMsg, `http_${response.status}`); } const text = await response.text(); const duration = Date.now() - startTime; logger.info(`AI 回复 integrateId=${currentConfig.integrateId} length=${text.length} duration=${duration}ms`); return text; } catch (err) { if (err instanceof CskError) { throw err; } logger.error(`请求异常 integrateId=${currentConfig.integrateId}`, err); throw new CskError('请求发生未知错误', 'unknown'); } } /** * SSE 流式对话请求 * @param message 用户消息 * @param onChunk 每次收到文本片段的回调 * @param onDone 流结束时的回调 * @param onError 发生错误时的回调 */ async function chatSSERequest(message, onChunk, onDone, onError) { var _a; const url = buildChatSSEUrl(message); const startTime = Date.now(); let totalText = ''; logger.info(`发送流式消息 integrateId=${currentConfig.integrateId} length=${message.length}`); try { const response = await safeFetch(url, {}, REQUEST_TIMEOUT * 2); // SSE 超时更长 if (!response.ok) { const errorMsg = getHttpErrorMessage(response.status); logger.error(`流式请求失败 integrateId=${currentConfig.integrateId} status=${response.status} message=${errorMsg}`); onError(new CskError(errorMsg, `http_${response.status}`)); return; } const reader = (_a = response.body) === null || _a === void 0 ? void 0 : _a.getReader(); if (!reader) { onError(new CskError('浏览器不支持流式读取', 'stream_unsupported')); return; } const decoder = new TextDecoder('utf-8', { stream: true }); let buffer = ''; try { while (true) { const { done, value } = await reader.read(); if (done) { break; } // 解码并追加到 buffer buffer += decoder.decode(value, { stream: true }); // 按行解析 SSE 数据 const lines = buffer.split('\n'); // 最后一行可能不完整,保留到下次 buffer = lines.pop() || ''; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith(':')) { // 跳过空行和 SSE 注释 continue; } // 解析 "data: xxx" 格式 if (trimmed.startsWith('data:')) { const data = trimmed.substring(5).trim(); if (data) { totalText += data; onChunk(data); } } else if (trimmed === '[DONE]') { // 流结束标记(OpenAI 风格,此处做兼容) break; } else if (!trimmed.startsWith('event:') && !trimmed.startsWith('id:') && !trimmed.startsWith('retry:')) { // 可能是 Flux 裸文本格式(无 data: 前缀) totalText += trimmed; onChunk(trimmed); } } } // 处理 buffer 中剩余的数据 if (buffer.trim()) { const trimmed = buffer.trim(); if (trimmed.startsWith('data:')) { const data = trimmed.substring(5).trim(); if (data) { totalText += data; onChunk(data); } } else if (trimmed !== '[DONE]') { totalText += trimmed; onChunk(trimmed); } } } catch (readErr) { // 流中断不丢已接收的文本 if (totalText.length > 0) { onChunk('\n\n[网络不稳定,内容可能不完整]'); } else { throw readErr; } } finally { reader.releaseLock(); } const duration = Date.now() - startTime; logger.info(`流式回复完成 integrateId=${currentConfig.integrateId} length=${totalText.length} duration=${duration}ms`); onDone(); } catch (err) { if (err instanceof CskError) { onError(err); } else { logger.error(`流式请求异常 integrateId=${currentConfig.integrateId}`, err); onError(new CskError('网络连接失败,请检查网络', 'network')); } } } let styleElement = null; /** CSS 变量:将配置中的主题色转换为 CSS 自定义属性 */ function cssVars(config) { // 简单的主色调加深(hover 用) const darker = adjustColor(config.primaryColor, -15); return ` --csk-primary: ${config.primaryColor}; --csk-primary-hover: ${darker}; --csk-bg-user: var(--csk-primary); --csk-bg-ai: #F3F4F6; --csk-text-user: #FFFFFF; --csk-text-ai: #1F2937; --csk-window-width: ${config.width}px; `; } /** 简单的颜色加深(HSL 方式) */ function adjustColor(hex, amount) { // 如果是 hex 格式,简单地对每个通道加减 const match = hex.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/); if (!match) { return hex; } const clamp = (v) => Math.max(0, Math.min(255, v)); const r = clamp(parseInt(match[1], 16) + amount); const g = clamp(parseInt(match[2], 16) + amount); const b = clamp(parseInt(match[3], 16) + amount); return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; } /** 完整 CSS 样式表 */ function getStyles(config) { return ` /* ChatbotSDK 样式 - csk- 命名空间 */ .csk-root { ${cssVars(config)} font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", sans-serif; font-size: 14px; line-height: 1.5; color: #1F2937; } /* ========== 悬浮按钮 ========== */ .csk-launcher { position: fixed; bottom: 20px; z-index: 9998; width: 56px; height: 56px; border-radius: 50%; background: #fff; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; border: none; color: var(--csk-primary); user-select: none; } .csk-launcher--right { right: 20px; } .csk-launcher--left { left: 20px; } .csk-launcher:hover { transform: scale(1.1); box-shadow: 0 6px 24px rgba(0, 0, 0, 0.2); } .csk-launcher:active { transform: scale(0.95); } /* ========== 聊天弹窗 ========== */ .csk-window { position: fixed; bottom: 20px; z-index: 9999; width: var(--csk-window-width); height: 560px; background: #fff; border-radius: 12px; box-shadow: 0 8px 40px rgba(0, 0, 0, 0.18); display: flex; flex-direction: column; overflow: hidden; transition: opacity 0.2s ease, transform 0.2s ease; } .csk-window--right { right: 20px; } .csk-window--left { left: 20px; } .csk-window--hidden { display: none; } /* ========== 头部 ========== */ .csk-header { display: flex; align-items: center; justify-content: space-between; padding: 0 16px; height: 48px; min-height: 48px; background: var(--csk-primary); color: #fff; border-radius: 12px 12px 0 0; cursor: move; user-select: none; } .csk-header__title { font-size: 15px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .csk-header__actions { display: flex; align-items: center; gap: 4px; } .csk-header__btn { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border: none; background: transparent; color: #fff; cursor: pointer; border-radius: 6px; transition: background 0.15s; } .csk-header__btn:hover { background: rgba(255, 255, 255, 0.2); } /* ========== 消息区 ========== */ .csk-messages { flex: 1; overflow-y: auto; padding: 16px; background: #FAFAFA; scroll-behavior: smooth; } .csk-messages::-webkit-scrollbar { width: 5px; } .csk-messages::-webkit-scrollbar-track { background: transparent; } .csk-messages::-webkit-scrollbar-thumb { background: #D1D5DB; border-radius: 3px; } /* 消息气泡 */ .csk-msg { display: flex; flex-direction: column; margin-bottom: 16px; max-width: 85%; word-break: break-word; } .csk-msg--user { margin-left: auto; align-items: flex-end; } .csk-msg--ai { margin-right: auto; align-items: flex-start; } .csk-msg__bubble { padding: 10px 14px; border-radius: 12px; font-size: 14px; line-height: 1.6; } .csk-msg--user .csk-msg__bubble { background: var(--csk-bg-user); color: var(--csk-text-user); border-radius: 12px 12px 4px 12px; } .csk-msg--ai .csk-msg__bubble { background: var(--csk-bg-ai); color: var(--csk-text-ai); border-radius: 12px 12px 12px 4px; } .csk-msg__time { font-size: 11px; color: #9CA3AF; margin-top: 4px; padding: 0 4px; } /* ========== Loading 动画 ========== */ .csk-loading { display: flex; align-items: center; gap: 6px; padding: 12px 14px; margin-bottom: 16px; } .csk-loading__dot { width: 8px; height: 8px; border-radius: 50%; background: #D1D5DB; animation: csk-bounce 1.4s ease-in-out infinite both; } .csk-loading__dot:nth-child(1) { animation-delay: 0s; } .csk-loading__dot:nth-child(2) { animation-delay: 0.16s; } .csk-loading__dot:nth-child(3) { animation-delay: 0.32s; } @keyframes csk-bounce { 0%, 80%, 100% { transform: scale(0.6); } 40% { transform: scale(1); } } /* ========== 输入区 ========== */ .csk-input-area { display: flex; align-items: center; padding: 10px 12px; border-top: 1px solid #E5E7EB; background: #fff; gap: 8px; } .csk-input { flex: 1; border: 1px solid #E5E7EB; border-radius: 8px; padding: 10px 12px; font-size: 14px; outline: none; transition: border-color 0.2s; font-family: inherit; resize: none; min-height: 20px; max-height: 100px; } .csk-input:focus { border-color: var(--csk-primary); } .csk-input::placeholder { color: #9CA3AF; } .csk-send-btn { display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; min-width: 40px; border: none; border-radius: 8px; background: var(--csk-primary); color: #fff; cursor: pointer; transition: background 0.2s; } .csk-send-btn:hover { background: var(--csk-primary-hover); } .csk-send-btn:disabled { background: #D1D5DB; cursor: not-allowed; } /* ========== 清空按钮 ========== */ .csk-clear-btn { display: inline-flex; align-items: center; gap: 4px; padding: 6px 12px; border: 1px solid #E5E7EB; border-radius: 6px; background: #fff; color: #6B7280; font-size: 12px; cursor: pointer; margin: 0 auto 8px; transition: all 0.15s; } .csk-clear-btn:hover { background: #FEE2E2; border-color: #FCA5A5; color: #DC2626; } /* ========== 移动端适配 ========== */ @media (max-width: 480px) { .csk-window { width: 100vw !important; height: 100vh !important; bottom: 0 !important; right: 0 !important; left: 0 !important; border-radius: 0; } .csk-header { border-radius: 0; } } `; } /** * 注入样式到 document.head */ function injectStyles(config) { // 避免重复注入 if (document.querySelector('style[data-csk-sdk]')) { return; } styleElement = document.createElement('style'); styleElement.setAttribute('data-csk-sdk', ''); styleElement.textContent = getStyles(config); document.head.appendChild(styleElement); } /** * 移除注入的样式 */ function removeStyles() { if (styleElement && styleElement.parentNode) { styleElement.parentNode.removeChild(styleElement); styleElement = null; } // 同时移除可能残留的其他 style 标签 document.querySelectorAll('style[data-csk-sdk]').forEach((el) => el.remove()); } /** * 工具函数模块 */ /** 生成简短 UUID(取 crypto.randomUUID 前 8 位) */ /** 生成完整 UUID */ function uuid() { if (typeof crypto !== 'undefined' && crypto.randomUUID) { return crypto.randomUUID(); } // fallback return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } /** 防抖函数 */ function debounce(fn, delay) { let timer = null; return function (...args) { if (timer !== null) { clearTimeout(timer); } timer = setTimeout(() => { fn.apply(this, args); timer = null; }, delay); }; } /** 获取当前时间戳(毫秒) */ function now() { return Date.now(); } // ==================== 悬浮按钮 ==================== /** 创建悬浮按钮 */ function createLauncher(config, onClick) { const launcher = document.createElement('div'); launcher.id = 'csk-launcher'; launcher.className = `csk-launcher csk-launcher--${config.position === 'left-bottom' ? 'left' : 'right'}`; launcher.setAttribute('title', config.title); launcher.setAttribute('aria-label', config.title); launcher.setAttribute('role', 'button'); launcher.setAttribute('tabindex', '0'); // 图标内容 launcher.innerHTML = config.launcherIcon; // 点击事件(300ms 防抖) const debouncedClick = debounce(onClick, 300); launcher.addEventListener('click', debouncedClick); // 键盘支持 launcher.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); debouncedClick(); } }); return launcher; } // ==================== 聊天弹窗 ==================== /** 创建聊天弹窗完整结构,返回各区域引用 */ function createChatWindow(config) { // 最外层容器 const windowEl = document.createElement('div'); windowEl.id = 'csk-window'; windowEl.className = `csk-root csk-window csk-window--${config.position === 'left-bottom' ? 'left' : 'right'} csk-window--hidden`; // === 头部 === const header = document.createElement('div'); header.className = 'csk-header'; const titleEl = document.createElement('span'); titleEl.className = 'csk-header__title'; titleEl.textContent = config.title; const actions = document.createElement('div'); actions.className = 'csk-header__actions'; // 最小化按钮 const minimizeBtn = document.createElement('button'); minimizeBtn.className = 'csk-header__btn csk-header__btn--minimize'; minimizeBtn.setAttribute('title', '最小化'); minimizeBtn.innerHTML = ``; minimizeBtn.addEventListener('click', () => { windowEl.classList.add('csk-window--hidden'); }); // 关闭按钮 const closeBtn = document.createElement('button'); closeBtn.className = 'csk-header__btn csk-header__btn--close'; closeBtn.setAttribute('title', '关闭'); closeBtn.innerHTML = ``; closeBtn.addEventListener('click', () => { windowEl.classList.add('csk-window--hidden'); }); actions.appendChild(minimizeBtn); actions.appendChild(closeBtn); header.appendChild(titleEl); header.appendChild(actions); // === 消息区 === const messagesContainer = document.createElement('div'); messagesContainer.id = 'csk-messages'; messagesContainer.className = 'csk-messages'; // === 输入区 === const inputArea = document.createElement('div'); inputArea.className = 'csk-input-area'; const inputEl = document.createElement('textarea'); inputEl.id = 'csk-input'; inputEl.className = 'csk-input'; inputEl.setAttribute('placeholder', '输入您的问题...'); inputEl.setAttribute('rows', '1'); inputEl.setAttribute('autofocus', ''); const sendBtn = document.createElement('button'); sendBtn.id = 'csk-send-btn'; sendBtn.className = 'csk-send-btn'; sendBtn.setAttribute('title', '发送'); sendBtn.setAttribute('disabled', 'true'); sendBtn.innerHTML = ``; inputArea.appendChild(inputEl); inputArea.appendChild(sendBtn); // === 组装 === windowEl.appendChild(header); windowEl.appendChild(messagesContainer); windowEl.appendChild(inputArea); // 清空按钮(可选) let clearBtn = null; if (config.showClear) { clearBtn = document.createElement('button'); clearBtn.className = 'csk-clear-btn'; clearBtn.textContent = '清空对话'; clearBtn.style.display = 'none'; // 初始隐藏,有消息后才显示 // 插入到 messages 之后、inputArea 之前 windowEl.insertBefore(clearBtn, inputArea); } // === Loading 动画 === let loadingEl = null; function showLoading() { if (loadingEl) { loadingEl.style.display = 'flex'; return loadingEl; } const el = document.createElement('div'); el.className = 'csk-loading'; el.innerHTML = `
`; messagesContainer.appendChild(el); loadingEl = el; return el; } function hideLoading() { if (loadingEl && loadingEl.parentNode) { loadingEl.parentNode.removeChild(loadingEl); loadingEl = null; } } return { window: windowEl, messagesContainer, inputEl, sendBtn, clearBtn, showLoading, hideLoading, }; } // ==================== 拖拽支持 ==================== /** 启用弹窗拖拽 */ function enableDrag(headerEl, windowEl) { let dragging = false; let startX = 0; let startY = 0; let offsetX = 0; let offsetY = 0; const onMouseDown = (e) => { dragging = true; startX = e.clientX; startY = e.clientY; const rect = windowEl.getBoundingClientRect(); offsetX = startX - rect.left; offsetY = startY - rect.top; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }; const onMouseMove = (e) => { if (!dragging) return; const x = e.clientX - offsetX; const y = e.clientY - offsetY; // 边界限制,防止拖出视口 const maxX = window.innerWidth - windowEl.offsetWidth; const maxY = window.innerHeight - windowEl.offsetHeight; windowEl.style.right = 'auto'; windowEl.style.bottom = 'auto'; windowEl.style.left = `${Math.max(0, Math.min(x, maxX))}px`; windowEl.style.top = `${Math.max(0, Math.min(y, maxY))}px`; }; const onMouseUp = () => { dragging = false; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); }; headerEl.addEventListener('mousedown', onMouseDown); // 清理函数 return () => { headerEl.removeEventListener('mousedown', onMouseDown); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); }; } // ==================== 消息渲染 ==================== /** 渲染用户消息气泡 */ function renderUserBubble(container, text, timestamp) { const wrapper = document.createElement('div'); wrapper.className = 'csk-msg csk-msg--user'; const bubble = document.createElement('div'); bubble.className = 'csk-msg__bubble'; bubble.textContent = text; const time = document.createElement('div'); time.className = 'csk-msg__time'; time.textContent = formatTime(timestamp); wrapper.appendChild(bubble); wrapper.appendChild(time); container.appendChild(wrapper); return wrapper; } /** 渲染 AI 消息气泡 */ function renderAIBubble(container, text, timestamp) { const wrapper = document.createElement('div'); wrapper.className = 'csk-msg csk-msg--ai'; const bubble = document.createElement('div'); bubble.className = 'csk-msg__bubble'; bubble.textContent = text; const time = document.createElement('div'); time.className = 'csk-msg__time'; time.textContent = formatTime(timestamp); wrapper.appendChild(bubble); wrapper.appendChild(time); container.appendChild(wrapper); return wrapper; } /** 创建空的 AI 气泡(流式追加用) */ function createEmptyAIBubble(container, timestamp) { const wrapper = document.createElement('div'); wrapper.className = 'csk-msg csk-msg--ai'; const bubble = document.createElement('div'); bubble.className = 'csk-msg__bubble'; bubble.innerHTML = ''; const time = document.createElement('div'); time.className = 'csk-msg__time'; time.textContent = formatTime(timestamp); wrapper.appendChild(bubble); wrapper.appendChild(time); container.appendChild(wrapper); return { wrapper, bubble }; } /** 滚动消息区到底部 */ function scrollToBottom(container) { container.scrollTop = container.scrollHeight; } /** 格式化时间戳 */ function formatTime(timestamp) { const d = new Date(timestamp); const hh = String(d.getHours()).padStart(2, '0'); const mm = String(d.getMinutes()).padStart(2, '0'); return `${hh}:${mm}`; } const STORAGE_PREFIX = 'csk_history_'; const MAX_MESSAGES = 200; const TRIM_COUNT = 50; /** 生成存储 key */ function storageKey(integrateId) { return `${STORAGE_PREFIX}${integrateId}`; } /** * 保存消息到 localStorage */ function saveMessages(integrateId, messages) { try { // 消息上限裁剪:保留最新 200 条,超出裁剪最早 50 条 let trimmed = messages; if (trimmed.length > MAX_MESSAGES) { trimmed = trimmed.slice(TRIM_COUNT); logger.warn(`消息数量达到上限,已裁剪最早 ${TRIM_COUNT} 条,当前 ${trimmed.length} 条`); } const data = { messages: trimmed, updatedAt: Date.now(), }; localStorage.setItem(storageKey(integrateId), JSON.stringify(data)); } catch (e) { if (e instanceof Error && e.name === 'QuotaExceededError') { logger.error('localStorage 空间不足,会话历史保存失败。建议清空历史记录。'); } else { logger.error('保存会话历史失败', e); } } } /** * 从 localStorage 加载消息 */ function loadMessages(integrateId) { try { const raw = localStorage.getItem(storageKey(integrateId)); if (!raw) { return []; } const data = JSON.parse(raw); if (!data || !Array.isArray(data.messages)) { return []; } logger.info(`加载历史消息 integrateId=${integrateId} count=${data.messages.length}`); return data.messages; } catch (e) { logger.warn('加载会话历史失败', e); return []; } } /** * 清空指定 integrateId 的本地缓存 */ function clearMessages(integrateId) { try { localStorage.removeItem(storageKey(integrateId)); } catch (e) { logger.warn('清空会话历史失败', e); } } let config$1 = null; let messages = []; let messagesContainer$1 = null; let inputEl$1 = null; let sendBtn$1 = null; let clearBtn$1 = null; let showLoadingFn$1 = null; let hideLoadingFn$1 = null; let isSending = false; /** * 初始化对话模块 */ function initChat(cfg, dom) { config$1 = cfg; messagesContainer$1 = dom.messagesContainer; inputEl$1 = dom.inputEl; sendBtn$1 = dom.sendBtn; clearBtn$1 = dom.clearBtn; showLoadingFn$1 = dom.showLoading; hideLoadingFn$1 = dom.hideLoading; // 绑定发送事件 bindSendEvents(); // 恢复历史消息 const history = loadMessages(cfg.integrateId); if (history.length > 0) { messages = history; renderHistory(); } } /** 绑定发送相关事件 */ function bindSendEvents() { if (!inputEl$1 || !sendBtn$1) return; // 发送按钮点击 sendBtn$1.addEventListener('click', () => { handleSend(); }); // 输入框键盘事件:回车发送 / Shift+Enter 换行 inputEl$1.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }); // 输入框内容变化时启用/禁用发送按钮 inputEl$1.addEventListener('input', () => { updateSendBtnState(); }); // 清空按钮 if (clearBtn$1) { clearBtn$1.addEventListener('click', () => { handleClear(); }); } } /** 更新发送按钮状态 */ function updateSendBtnState() { if (!sendBtn$1 || !inputEl$1) return; const hasText = inputEl$1.value.trim().length > 0; if (hasText && !isSending) { sendBtn$1.removeAttribute('disabled'); } else { sendBtn$1.setAttribute('disabled', 'true'); } } /** 处理发送消息 */ async function handleSend() { if (!inputEl$1 || !config$1 || isSending) return; const text = inputEl$1.value.trim(); if (text === '') return; // 清空输入框 inputEl$1.value = ''; updateSendBtnState(); // 自动调整 textarea 高度 inputEl$1.style.height = 'auto'; isSending = true; updateSendBtnState(); // 1. 渲染用户气泡 const userTimestamp = now(); if (messagesContainer$1) { renderUserBubble(messagesContainer$1, text, userTimestamp); } const userMsg = { id: uuid(), role: 'user', content: text, timestamp: userTimestamp, }; messages.push(userMsg); // 显示清空按钮 if (clearBtn$1 && messages.length > 0) { clearBtn$1.style.display = 'inline-flex'; } // 滚动到底部 if (messagesContainer$1) scrollToBottom(messagesContainer$1); // 2. 显示 loading if (showLoadingFn$1) showLoadingFn$1(); if (messagesContainer$1) scrollToBottom(messagesContainer$1); // 3. 发送请求 try { let aiContent; const aiTimestamp = now(); if (config$1.streaming) { // 流式输出 aiContent = await sendStreamMessage(text, aiTimestamp); } else { // 同步请求 aiContent = await chatRequest(text); } // 4. 隐藏 loading if (hideLoadingFn$1) hideLoadingFn$1(); // 5. 渲染 AI 气泡 if (messagesContainer$1) { renderAIBubble(messagesContainer$1, aiContent, aiTimestamp); } const aiMsg = { id: uuid(), role: 'ai', content: aiContent, timestamp: aiTimestamp, }; messages.push(aiMsg); // 6. 保存到 localStorage saveMessages(config$1.integrateId, messages); // 7. 滚动到底部 if (messagesContainer$1) scrollToBottom(messagesContainer$1); } catch (err) { // 隐藏 loading if (hideLoadingFn$1) hideLoadingFn$1(); // 渲染错误提示 const errMsg = err instanceof CskError ? err.message : '发送失败,请稍后重试'; if (messagesContainer$1) { const errorBubble = document.createElement('div'); errorBubble.className = 'csk-msg csk-msg--ai'; const bubble = document.createElement('div'); bubble.className = 'csk-msg__bubble'; bubble.style.color = '#DC2626'; bubble.textContent = `⚠ ${errMsg}`; errorBubble.appendChild(bubble); messagesContainer$1.appendChild(errorBubble); } logger.error(`发送失败 integrateId=${config$1.integrateId}`, err); } finally { isSending = false; updateSendBtnState(); } } /** 流式发送消息 */ async function sendStreamMessage(text, aiTimestamp) { return new Promise((resolve, reject) => { let bubbleEl = null; let wrapperEl = null; let accumulated = ''; let streamStarted = false; chatSSERequest(text, // onChunk (chunk) => { accumulated += chunk; if (!streamStarted && messagesContainer$1) { // 隐藏 loading,创建空 AI 气泡 if (hideLoadingFn$1) hideLoadingFn$1(); const { wrapper, bubble } = createEmptyAIBubble(messagesContainer$1, aiTimestamp); wrapperEl = wrapper; bubbleEl = bubble; streamStarted = true; } if (bubbleEl) { bubbleEl.textContent = accumulated; } if (messagesContainer$1) scrollToBottom(messagesContainer$1); }, // onDone () => { // 如果流没有产生任何内容,回退同步请求 if (!streamStarted && accumulated === '') { chatRequest(text) .then((content) => resolve(content)) .catch(reject); return; } resolve(accumulated); }, // onError (error) => { if (accumulated.length > 0) { // 有部分内容,保留并添加提示 if (bubbleEl) { bubbleEl.textContent = accumulated + '\n\n[回复被中断]'; } resolve(accumulated); } else { reject(error); } }); }); } /** 渲染历史消息 */ function renderHistory() { if (!messagesContainer$1) return; // 清空容器 messagesContainer$1.innerHTML = ''; for (const msg of messages) { if (msg.role === 'user') { renderUserBubble(messagesContainer$1, msg.content, msg.timestamp); } else { renderAIBubble(messagesContainer$1, msg.content, msg.timestamp); } } scrollToBottom(messagesContainer$1); // 显示清空按钮 if (clearBtn$1 && messages.length > 0) { clearBtn$1.style.display = 'inline-flex'; } } /** 清空对话历史 */ function handleClear() { if (!config$1) return; if (!confirm('确定清空所有对话记录?')) { return; } messages = []; if (messagesContainer$1) { messagesContainer$1.innerHTML = ''; } if (clearBtn$1) { clearBtn$1.style.display = 'none'; } clearMessages(config$1.integrateId); logger.info(`清空会话 integrateId=${config$1.integrateId}`); } // ==================== 单例状态 ==================== let config = null; let isInitialized = false; let launcherEl = null; let windowEl = null; let messagesContainer = null; let inputEl = null; let sendBtn = null; let clearBtn = null; let showLoadingFn = null; let hideLoadingFn = null; let dragCleanup = null; // ==================== 公开 API ==================== /** 初始化 SDK */ function init(rawConfig) { if (isInitialized) { logger.warn('SDK 已初始化,请先调用 destroy() 再重新初始化'); return; } // 1. 配置解析与校验 const parsed = parseConfig(rawConfig); if (!parsed) { return; // parseConfig 已输出错误 } config = parsed; // 2. 设置日志级别 setDebug(config.debug); // 3. 设置 API 配置 setApiConfig(config); // 4. 注入样式 injectStyles(config); // 5. 创建悬浮按钮 launcherEl = createLauncher(config, toggle); document.body.appendChild(launcherEl); // 6. 创建聊天弹窗 const dom = createChatWindow(config); windowEl = dom.window; messagesContainer = dom.messagesContainer; inputEl = dom.inputEl; sendBtn = dom.sendBtn; clearBtn = dom.clearBtn; showLoadingFn = dom.showLoading; hideLoadingFn = dom.hideLoading; document.body.appendChild(windowEl); // 7. 启用拖拽 const headerEl = windowEl.querySelector('.csk-header'); if (headerEl) { dragCleanup = enableDrag(headerEl, windowEl); } // 8. 初始化对话模块 initChat(config, { messagesContainer, inputEl, sendBtn, clearBtn, showLoading: showLoadingFn, hideLoading: hideLoadingFn, }); isInitialized = true; logger.info(`初始化完成 integrateId=${config.integrateId} requestDomain=${config.requestDomain}`); } /** 销毁 SDK 实例 */ function destroy() { if (!isInitialized) { return; } // 移除 DOM 元素 if (launcherEl && launcherEl.parentNode) { launcherEl.parentNode.removeChild(launcherEl); launcherEl = null; } if (windowEl && windowEl.parentNode) { windowEl.parentNode.removeChild(windowEl); windowEl = null; } // 移除拖拽事件 if (dragCleanup) { dragCleanup(); dragCleanup = null; } // 移除样式 removeStyles(); // 重置状态 const oldIntegrateId = config === null || config === void 0 ? void 0 : config.integrateId; config = null; isInitialized = false; messagesContainer = null; inputEl = null; sendBtn = null; clearBtn = null; showLoadingFn = null; hideLoadingFn = null; logger.info(`销毁实例 integrateId=${oldIntegrateId}`); } /** 打开聊天窗口 */ function open() { if (!windowEl) return; windowEl.classList.remove('csk-window--hidden'); } /** 关闭聊天窗口 */ function close() { if (!windowEl) return; windowEl.classList.add('csk-window--hidden'); } /** 切换窗口显示/隐藏 */ function toggle() { if (!windowEl) return; if (windowEl.classList.contains('csk-window--hidden')) { open(); // 聚焦输入框 setTimeout(() => { if (inputEl) inputEl.focus(); }, 100); } else { close(); } } /** 清空当前会话历史 */ function clearHistory() { if (!config) return; // 通过触发自定义事件,让 chat 模块处理 if (clearBtn) { clearBtn.click(); } else if (confirm('确定清空所有对话记录?')) { clearMessages(config.integrateId); } } // ==================== 挂载到全局 ==================== const ChatbotSDK = { init, destroy, open, close, toggle, clearHistory, }; // IIFE 自动挂载 if (typeof window !== 'undefined') { window.ChatbotSDK = ChatbotSDK; } return ChatbotSDK; })(); //# sourceMappingURL=chatbot-sdk.js.map