var ChatbotSDK = (function () { 'use strict'; const PREFIX = '[ChatbotSDK]'; let debugEnabled = true; /** 设置是否开启调试日志 */ function setDebug(enabled) { debugEnabled = enabled; } /** 性能计时器 */ const timers = {}; const logger = { /** 普通信息日志(受 debug 开关控制) */ info(msg, data) { if (debugEnabled) { console.log(PREFIX, msg, data !== undefined ? data : ''); } }, /** 警告日志(受 debug 开关控制) */ warn(msg, data) { if (debugEnabled) { console.warn(PREFIX, msg, data !== undefined ? data : ''); } }, /** 错误日志(始终输出,不受 debug 开关控制) */ error(msg, data) { console.error(PREFIX, msg, data !== undefined ? data : ''); }, /** 开始计时 */ time(label) { timers[label] = Date.now(); }, /** 结束计时并输出日志 */ timeEnd(label, prefix) { const start = timers[label]; if (start !== undefined) { const duration = Date.now() - start; delete timers[label]; if (debugEnabled) { const msg = prefix ? `${prefix} ${duration}ms` : `${label} ${duration}ms`; console.log(PREFIX, msg); } return duration; } return 0; }, /** 生命周期日志:init */ lifecycleInit(integrateId, requestDomain) { this.info(`初始化完成 integrateId=${integrateId} requestDomain=${requestDomain}`); }, /** 生命周期日志:destroy */ lifecycleDestroy(integrateId) { this.info(`销毁实例 integrateId=${integrateId}`); }, /** 生命周期日志:sendMessage */ lifecycleSend(integrateId, length) { this.info(`发送消息 integrateId=${integrateId} length=${length}`); this.time(`send_${integrateId}`); }, /** 生命周期日志:收到回复 */ lifecycleReply(integrateId, length) { const duration = this.timeEnd(`send_${integrateId}`, 'AI 回复'); this.info(`AI 回复 integrateId=${integrateId} length=${length} duration=${duration}ms`); }, /** 生命周期日志:请求失败 */ lifecycleError(integrateId, status, message) { this.timeEnd(`send_${integrateId}`); this.error(`请求失败 integrateId=${integrateId} status=${status} message=${message}`); }, /** 生命周期日志:清空会话 */ lifecycleClear(integrateId) { this.info(`清空会话 integrateId=${integrateId}`); }, /** 生命周期日志:流式回复完成 */ lifecycleStreamDone(integrateId, length) { const duration = this.timeEnd(`send_${integrateId}`, '流式回复'); this.info(`流式回复完成 integrateId=${integrateId} length=${length} duration=${duration}ms`); }, /** 生命周期日志:知识库切换 */ lifecycleCategoryChange(categoryId) { this.info(`切换知识库分类 categoryId=${categoryId}`); }, }; /** 默认悬浮按钮 SVG 图标(客服对话气泡) */ const DEFAULT_LAUNCHER_ICON = ``; /** * 解析并校验用户传入的配置,填充默认值 */ function parseConfig(raw) { var _a, _b, _c, _d, _e, _f; // 校验必传参数:integrateId(对应后端 roleId) if (!raw.integrateId || (typeof raw.integrateId !== 'string' && typeof raw.integrateId !== 'number') || (typeof raw.integrateId === 'string' && raw.integrateId.trim() === '')) { logger.error('integrateId 是必传参数(对应后端 roleId 客服角色 ID),请检查 init() 调用。示例:ChatbotSDK.init({ integrateId: 1, 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: 1, 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; } // integrateId 统一转为字符串(后端 roleId 为 Long,但 query param 传字符串也可接收) const integrateIdStr = String(raw.integrateId).trim(); // 填充默认值 const config = { integrateId: integrateIdStr, requestDomain: raw.requestDomain.replace(/\/+$/, ''), // 去掉末尾斜杠 userId: raw.userId, 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, chatId: '', // 初始为空,由 chatId 初始化流程填充 }; logger.info(`配置解析完成 integrateId(=roleId)=${config.integrateId} userId(=accountId)=${config.userId || '(未设置)'} requestDomain=${config.requestDomain}`); return config; } /** * 多语言国际化模块 - i18n 字典 + 翻译函数 */ /** 语言包字典 */ const dictionaries = { 'zh-CN': { // 头部 title: 'AI 智能助手', minimize: '最小化', close: '关闭', // 输入区 placeholder: '输入您的问题...', send: '发送', // 消息 loading: '正在思考...', stream_interrupted: '回复被中断', stream_unstable: '网络不稳定,内容可能不完整', // 知识库 category_placeholder: '选择知识库分类', category_all: '全部分类', category_load_error: '加载分类失败', source_title: '参考来源', source_count: '{n} 条参考来源', source_loading: '加载来源中...', // 清空/管理 clear: '清空对话', clear_confirm: '确定清空所有对话记录?', // 历史会话 history_title: '历史会话', history_empty: '暂无历史会话', history_load_error: '加载会话列表失败', history_delete_confirm: '确定删除该会话?', history_export: '导出', history_delete: '删除', // 错误提示 error_network: '网络连接失败,请检查网络', error_timeout: '请求超时,请稍后重试', error_server: '服务器异常,请稍后重试', error_cors: '跨域请求被拦截,请联系管理员将当前域名加入 API 白名单', error_auth: '鉴权失败,请联系管理员', error_forbidden: '无访问权限,请联系管理员配置', error_not_found: '请求的资源不存在', error_rate_limit: '请求过于频繁,请稍后重试', error_unavailable: '服务暂不可用,请稍后重试', error_unknown: '请求发生未知错误', error_send: '发送失败,请稍后重试', error_stream_unsupported: '浏览器不支持流式读取', }, 'en': { // Header title: 'AI Assistant', minimize: 'Minimize', close: 'Close', // Input placeholder: 'Type your question...', send: 'Send', // Messages loading: 'Thinking...', stream_interrupted: 'Response interrupted', stream_unstable: 'Network unstable, content may be incomplete', // Knowledge base category_placeholder: 'Select category', category_all: 'All categories', category_load_error: 'Failed to load categories', source_title: 'Sources', source_count: '{n} source(s)', source_loading: 'Loading sources...', // Clear/Management clear: 'Clear chat', clear_confirm: 'Clear all conversation history?', // History history_title: 'History', history_empty: 'No conversations yet', history_load_error: 'Failed to load conversations', history_delete_confirm: 'Delete this conversation?', history_export: 'Export', history_delete: 'Delete', // Errors error_network: 'Network connection failed', error_timeout: 'Request timed out, please try again', error_server: 'Server error, please try again later', error_cors: 'CORS request blocked. Please contact admin to whitelist your domain', error_auth: 'Authentication failed, please contact admin', error_forbidden: 'Access denied, please contact admin', error_not_found: 'Resource not found', error_rate_limit: 'Too many requests, please try again later', error_unavailable: 'Service temporarily unavailable', error_unknown: 'Unknown request error', error_send: 'Failed to send, please try again', error_stream_unsupported: 'Browser does not support streaming', }, }; /** 当前语言 */ let currentLocale = 'zh-CN'; /** * 设置当前语言 */ function setLocale(locale) { if (dictionaries[locale]) { currentLocale = locale; } else { // 尝试匹配语言前缀(如 zh -> zh-CN) const prefix = locale.split('-')[0]; const matched = Object.keys(dictionaries).find(k => k.startsWith(prefix)); if (matched) { currentLocale = matched; } // 未匹配则保持默认 zh-CN } } /** * 获取翻译文本 * @param key 翻译 key * @param params 插值参数,如 { n: 3 } 替换 {n} */ function t(key, params) { const dict = dictionaries[currentLocale] || dictionaries['zh-CN']; let text = dict[key] || dictionaries['zh-CN'][key] || key; // 简单插值替换:{n} → 实际值 if (params) { for (const [k, v] of Object.entries(params)) { text = text.replace(`{${k}}`, String(v)); } } return text; } /** 请求超时时间(毫秒) */ const REQUEST_TIMEOUT = 30000; let currentConfig = null; /** 设置当前配置 */ function setApiConfig(config) { currentConfig = config; } /** 更新当前 chatId(对话 ID) */ function updateChatId(chatId) { if (currentConfig) { currentConfig.chatId = chatId; } } /** 获取当前 chatId */ function getChatId() { return (currentConfig === null || currentConfig === void 0 ? void 0 : currentConfig.chatId) || ''; } /** 构建完整请求 URL,自动防御双斜杠 */ function buildUrl(path) { if (!currentConfig) { throw new Error('API 配置未初始化'); } const domain = currentConfig.requestDomain.replace(/\/+$/, ''); const cleanPath = path.startsWith('/') ? path : `/${path}`; return `${domain}${cleanPath}`; } /** * 安全设置可选参数:仅当 value 非空时追加 */ function setIfPresent(params, key, value) { if (value === undefined || value === null) return; if (typeof value === 'string' && value.trim() === '') return; params.set(key, String(value)); } // ==================== 对话接口 URL 构建 ==================== /** * 构建同步对话请求 URL * - integrateId → roleId * - userId → accountId * - chatId → 自动管理的对话 ID */ function buildChatUrl(message) { const params = new URLSearchParams(); params.set('message', message); params.set('chatId', currentConfig.chatId); // integrateId 映射为 roleId setIfPresent(params, 'roleId', currentConfig.integrateId); // userId 映射为 accountId setIfPresent(params, 'accountId', currentConfig.userId); return buildUrl(`/ai/assistant_app/chat/sync?${params.toString()}`); } /** * 构建 SSE 流式请求 URL */ function buildChatSSEUrl(message, categoryId) { const params = new URLSearchParams(); params.set('message', message); params.set('chatId', currentConfig.chatId); setIfPresent(params, 'roleId', currentConfig.integrateId); setIfPresent(params, 'accountId', currentConfig.userId); setIfPresent(params, 'categoryId', categoryId !== null && categoryId !== void 0 ? categoryId : currentConfig.categoryId); return buildUrl(`/ai/assistant_app/chat/sse?${params.toString()}`); } /** * 构建 RAG 增强流式请求 URL */ function buildChatRAGSSEUrl(message, categoryId) { const params = new URLSearchParams(); params.set('message', message); params.set('chatId', currentConfig.chatId); params.set('rewriteStrategy', 'REWRITE'); setIfPresent(params, 'roleId', currentConfig.integrateId); setIfPresent(params, 'accountId', currentConfig.userId); setIfPresent(params, 'categoryId', categoryId !== null && categoryId !== void 0 ? categoryId : currentConfig.categoryId); return buildUrl(`/ai/assistant_app/chat/rag/sse?${params.toString()}`); } /** * 构建 RAG 引用来源请求 URL */ function buildRagSourcesUrl(message, categoryId) { const params = new URLSearchParams(); params.set('message', message); params.set('chatId', currentConfig.chatId); params.set('rewriteStrategy', 'REWRITE'); setIfPresent(params, 'roleId', currentConfig.integrateId); setIfPresent(params, 'accountId', currentConfig.userId); setIfPresent(params, 'categoryId', categoryId !== null && categoryId !== void 0 ? categoryId : currentConfig.categoryId); return buildUrl(`/ai/assistant_app/rag/sources?${params.toString()}`); } // ==================== HTTP 基础封装 ==================== /** 带超时的 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(t('error_timeout'), 'timeout'); } if (err instanceof TypeError && err.message.includes('Failed to fetch')) { throw new CskError(t('error_cors'), 'cors'); } throw new CskError(t('error_network'), '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 t('error_auth'); case 403: return t('error_forbidden'); case 404: return t('error_not_found'); case 429: return t('error_rate_limit'); case 500: return t('error_server'); case 502: case 503: return t('error_unavailable'); default: return `${t('error_unknown')}(${status})`; } } // ==================== 对话请求 ==================== /** * 同步对话请求 */ async function chatRequest(message) { const url = buildChatUrl(message); logger.lifecycleSend(currentConfig.integrateId, message.length); try { const response = await safeFetch(url); if (!response.ok) { const errorMsg = getHttpErrorMessage(response.status); logger.lifecycleError(currentConfig.integrateId, String(response.status), errorMsg); throw new CskError(errorMsg, `http_${response.status}`); } const text = await response.text(); logger.lifecycleReply(currentConfig.integrateId, text.length); return text; } catch (err) { if (err instanceof CskError) throw err; logger.lifecycleError(currentConfig.integrateId, 'unknown', String(err)); throw new CskError(t('error_unknown'), 'unknown'); } } /** * SSE 流式对话请求 * @param useRag 是否使用 RAG 增强对话 * @param categoryId 知识库分类 ID */ async function chatSSERequest(message, onChunk, onDone, onError, categoryId, useRag) { var _a; const url = useRag ? buildChatRAGSSEUrl(message, categoryId) : buildChatSSEUrl(message, categoryId); let totalText = ''; logger.lifecycleSend(currentConfig.integrateId, message.length); try { const response = await safeFetch(url, {}, REQUEST_TIMEOUT * 2); if (!response.ok) { const errorMsg = getHttpErrorMessage(response.status); logger.lifecycleError(currentConfig.integrateId, String(response.status), 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(t('error_stream_unsupported'), '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 += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith(':')) continue; if (trimmed.startsWith('data:')) { const data = trimmed.substring(5).trim(); if (data) { totalText += data; onChunk(data); } } else if (trimmed === '[DONE]') { break; } else if (!trimmed.startsWith('event:') && !trimmed.startsWith('id:') && !trimmed.startsWith('retry:')) { totalText += trimmed; onChunk(trimmed); } } } 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' + t('stream_unstable')); } else { throw readErr; } } finally { reader.releaseLock(); } logger.lifecycleStreamDone(currentConfig.integrateId, totalText.length); onDone(); } catch (err) { if (err instanceof CskError) { onError(err); } else { logger.lifecycleError(currentConfig.integrateId, 'unknown', String(err)); onError(new CskError(t('error_network'), 'network')); } } } // ==================== P1: 知识库分类 ==================== /** * 获取知识库分类树 */ async function fetchCategoryTree() { const url = buildUrl('/category/tree'); try { const response = await safeFetch(url); if (!response.ok) throw new CskError(getHttpErrorMessage(response.status), `http_${response.status}`); const json = await response.json(); if (json.success && Array.isArray(json.data)) { logger.info(`加载分类树成功 count=${json.data.length}`); return json.data; } return []; } catch (err) { if (err instanceof CskError) logger.error(`加载分类树失败: ${err.message}`); else logger.error('加载分类树失败', err); return []; } } /** * 获取 RAG 引用来源 */ async function fetchRagSources(message, categoryId) { const url = buildRagSourcesUrl(message, categoryId); try { const response = await safeFetch(url); if (!response.ok) throw new CskError(getHttpErrorMessage(response.status), `http_${response.status}`); const json = await response.json(); if (json.success && Array.isArray(json.data)) { logger.info(`获取引用来源 count=${json.data.length}`); return json.data; } return []; } catch (err) { logger.error('获取引用来源失败', err); return []; } } /** * 获取会话列表 */ async function fetchConversationList(page = 1, size = 20, accountId, roleId) { let path = `/conversation/list?page=${page}&size=${size}`; if (accountId) path += `&accountId=${encodeURIComponent(accountId)}`; if (roleId) path += `&roleId=${encodeURIComponent(roleId)}`; const url = buildUrl(path); try { const response = await safeFetch(url); if (!response.ok) throw new CskError(getHttpErrorMessage(response.status), `http_${response.status}`); const json = await response.json(); return { list: json.success && Array.isArray(json.data) ? json.data : [], total: json.total || 0, pages: json.pages || 0, }; } catch (err) { logger.error('加载会话列表失败', err); return { list: [], total: 0, pages: 0 }; } } /** * 获取会话消息 */ async function fetchConversationMessages(conversationId) { const url = buildUrl(`/conversation/${conversationId}/messages`); try { const response = await safeFetch(url); if (!response.ok) throw new CskError(getHttpErrorMessage(response.status), `http_${response.status}`); const json = await response.json(); return { messages: json.success && Array.isArray(json.data) ? json.data : [], total: json.total || 0, }; } catch (err) { logger.error('加载会话消息失败', err); return { messages: [], total: 0 }; } } /** * 删除会话 */ async function deleteConversation(conversationId) { const url = buildUrl(`/conversation/${conversationId}`); try { const response = await safeFetch(url, { method: 'DELETE' }); if (!response.ok) throw new CskError(getHttpErrorMessage(response.status), `http_${response.status}`); const json = await response.json(); logger.info(`删除会话 id=${conversationId} success=${json.success}`); return json.success || false; } catch (err) { logger.error('删除会话失败', err); return false; } } /** * 导出会话 URL */ function getConversationExportUrl(conversationId) { return buildUrl(`/conversation/${conversationId}/export`); } // ==================== chatId 自动初始化 ==================== /** * 初始化 chatId:查询后端已有会话,找到则复用,否则生成新的 * * 逻辑: * 1. 先查 localStorage 缓存的 chatId(同一 integrateId + userId 可能复用) * 2. 查 /conversation/list?accountId=X&roleId=Y 看是否有匹配的会话 * 3. 有会话 → 使用最新会话的 conversationId 作为 chatId * 4. 无会话 → 自动生成 chatId(格式:sdk_timestamp_random) */ async function initChatId() { if (!currentConfig) return ''; // 1. 先尝试从 localStorage 恢复 const cachedChatId = loadCachedChatId(currentConfig.integrateId, currentConfig.userId); if (cachedChatId) { currentConfig.chatId = cachedChatId; logger.info(`从缓存恢复 chatId=${cachedChatId}`); return cachedChatId; } // 2. 查询后端会话列表 try { const result = await fetchConversationList(1, 5, currentConfig.userId, currentConfig.integrateId); if (result.list.length > 0) { // 使用最新会话的 conversationId 作为 chatId const latestConv = result.list[0]; const chatId = latestConv.conversationId || latestConv.chatId || ''; if (chatId) { currentConfig.chatId = chatId; saveCachedChatId(currentConfig.integrateId, currentConfig.userId, chatId); logger.info(`从后端恢复会话 chatId=${chatId} messageCount=${latestConv.messageCount}`); return chatId; } } } catch (err) { logger.warn('查询后端会话列表失败,将生成新 chatId', err); } // 3. 生成新的 chatId const newChatId = generateChatId(); currentConfig.chatId = newChatId; saveCachedChatId(currentConfig.integrateId, currentConfig.userId, newChatId); logger.info(`生成新 chatId=${newChatId}`); return newChatId; } /** 生成 chatId(格式:sdk_timestamp_random) */ function generateChatId() { const random = typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID().substring(0, 8) : Math.random().toString(36).substring(2, 10); return `sdk_${Date.now()}_${random}`; } /** localStorage key 格式 */ function chatIdStorageKey(integrateId, userId) { return `csk_chatId_${integrateId}${userId ? '_' + userId : ''}`; } /** 从 localStorage 加载 chatId */ function loadCachedChatId(integrateId, userId) { try { return localStorage.getItem(chatIdStorageKey(integrateId, userId)) || ''; } catch (_a) { return ''; } } /** 保存 chatId 到 localStorage */ function saveCachedChatId(integrateId, userId, chatId) { try { if (chatId) { localStorage.setItem(chatIdStorageKey(integrateId, userId), chatId); } else { localStorage.removeItem(chatIdStorageKey(integrateId, userId)); } } catch (_a) { // localStorage 不可用则忽略 } } 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; } /* ========== P1: 知识库分类下拉 ========== */ .csk-category-bar { display: flex; align-items: center; padding: 6px 12px; border-top: 1px solid #E5E7EB; background: #F9FAFB; gap: 8px; } .csk-category-bar__label { font-size: 12px; color: #6B7280; white-space: nowrap; } .csk-category-select { flex: 1; padding: 5px 8px; border: 1px solid #E5E7EB; border-radius: 6px; font-size: 12px; color: #374151; background: #fff; outline: none; cursor: pointer; font-family: inherit; transition: border-color 0.2s; max-width: 200px; } .csk-category-select:focus { border-color: var(--csk-primary); } /* ========== P1: RAG 引用来源卡片 ========== */ .csk-sources { margin-top: 8px; border: 1px solid #E5E7EB; border-radius: 8px; overflow: hidden; font-size: 12px; max-width: 100%; } .csk-sources__header { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: #F9FAFB; cursor: pointer; user-select: none; transition: background 0.15s; } .csk-sources__header:hover { background: #F3F4F6; } .csk-sources__title { display: flex; align-items: center; gap: 4px; font-weight: 500; color: #374151; } .csk-sources__arrow { transition: transform 0.2s; color: #9CA3AF; } .csk-sources--collapsed .csk-sources__arrow { transform: rotate(-90deg); } .csk-sources__body { border-top: 1px solid #E5E7EB; padding: 0; } .csk-sources--collapsed .csk-sources__body { display: none; } .csk-source-item { padding: 8px 12px; border-bottom: 1px solid #F3F4F6; transition: background 0.15s; } .csk-source-item:last-child { border-bottom: none; } .csk-source-item:hover { background: #F9FAFB; } .csk-source-item__name { font-weight: 500; color: #1F2937; margin-bottom: 2px; } .csk-source-item__snippet { color: #6B7280; line-height: 1.4; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } .csk-source-item__meta { font-size: 11px; color: #9CA3AF; margin-top: 2px; } /* ========== P1: Markdown 渲染样式 ========== */ .csk-msg--ai .csk-msg__bubble .csk-md-p { margin: 0 0 8px; } .csk-msg--ai .csk-msg__bubble .csk-md-p:last-child { margin-bottom: 0; } .csk-msg--ai .csk-msg__bubble .csk-md-h1, .csk-msg--ai .csk-msg__bubble .csk-md-h2, .csk-msg--ai .csk-msg__bubble .csk-md-h3, .csk-msg--ai .csk-msg__bubble .csk-md-h4, .csk-msg--ai .csk-msg__bubble .csk-md-h5, .csk-msg--ai .csk-msg__bubble .csk-md-h6 { margin: 12px 0 6px; font-weight: 600; line-height: 1.3; } .csk-msg--ai .csk-msg__bubble .csk-md-h1 { font-size: 20px; } .csk-msg--ai .csk-msg__bubble .csk-md-h2 { font-size: 17px; } .csk-msg--ai .csk-msg__bubble .csk-md-h3 { font-size: 15px; } .csk-msg--ai .csk-msg__bubble .csk-md-h4 { font-size: 14px; } .csk-md-code-block { background: #1E293B; color: #E2E8F0; padding: 12px 14px; border-radius: 8px; overflow-x: auto; margin: 8px 0; font-size: 13px; line-height: 1.5; font-family: 'SF Mono', 'Consolas', 'Menlo', monospace; } .csk-md-code-block code { background: none; padding: 0; border-radius: 0; font-size: inherit; color: inherit; } .csk-md-inline-code { background: #E5E7EB; color: #DC2626; padding: 1px 6px; border-radius: 4px; font-size: 13px; font-family: 'SF Mono', 'Consolas', 'Menlo', monospace; } .csk-msg--ai .csk-msg__bubble .csk-md-ul, .csk-msg--ai .csk-msg__bubble .csk-md-ol { padding-left: 20px; margin: 6px 0; } .csk-msg--ai .csk-msg__bubble .csk-md-ul li, .csk-msg--ai .csk-msg__bubble .csk-md-ol li { margin-bottom: 4px; } .csk-md-blockquote { border-left: 3px solid var(--csk-primary); padding-left: 12px; margin: 8px 0; color: #6B7280; } .csk-md-link { color: var(--csk-primary); text-decoration: none; } .csk-md-link:hover { text-decoration: underline; } .csk-md-hr { border: none; border-top: 1px solid #E5E7EB; margin: 12px 0; } /* ========== P2: 会话管理面板 ========== */ .csk-history-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-history-btn:hover { background: rgba(255, 255, 255, 0.2); } .csk-history-panel { position: absolute; top: 48px; left: 0; right: 0; bottom: 0; background: #fff; z-index: 10; display: flex; flex-direction: column; overflow: hidden; } .csk-history-panel--hidden { display: none; } .csk-history-panel__header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid #E5E7EB; background: #F9FAFB; } .csk-history-panel__title { font-size: 14px; font-weight: 600; color: #1F2937; } .csk-history-panel__back { display: flex; align-items: center; gap: 4px; padding: 4px 10px; border: 1px solid #E5E7EB; border-radius: 6px; background: #fff; color: #374151; font-size: 12px; cursor: pointer; transition: all 0.15s; } .csk-history-panel__back:hover { background: #F3F4F6; } .csk-history-panel__list { flex: 1; overflow-y: auto; padding: 8px; } .csk-history-panel__list::-webkit-scrollbar { width: 4px; } .csk-history-panel__list::-webkit-scrollbar-thumb { background: #E5E7EB; border-radius: 2px; } .csk-history-item { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; border-radius: 8px; cursor: pointer; transition: background 0.15s; margin-bottom: 4px; } .csk-history-item:hover { background: #F3F4F6; } .csk-history-item__info { flex: 1; min-width: 0; } .csk-history-item__id { font-size: 13px; font-weight: 500; color: #1F2937; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .csk-history-item__meta { font-size: 11px; color: #9CA3AF; margin-top: 2px; } .csk-history-item__actions { display: flex; gap: 4px; margin-left: 8px; opacity: 0; transition: opacity 0.15s; } .csk-history-item:hover .csk-history-item__actions { opacity: 1; } .csk-history-action { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: none; border-radius: 6px; cursor: pointer; font-size: 12px; transition: all 0.15s; } .csk-history-action--export { background: #EFF6FF; color: #2563EB; } .csk-history-action--export:hover { background: #DBEAFE; } .csk-history-action--delete { background: #FEF2F2; color: #DC2626; } .csk-history-action--delete:hover { background: #FEE2E2; } .csk-history-panel__empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 20px; color: #9CA3AF; font-size: 13px; text-align: center; } .csk-history-panel__empty-icon { font-size: 32px; margin-bottom: 8px; opacity: 0.5; } .csk-history-panel__loading { display: flex; align-items: center; justify-content: center; padding: 20px; color: #9CA3AF; font-size: 13px; } .csk-history-panel__loadmore { display: block; width: 100%; padding: 10px; border: none; background: #F9FAFB; color: #6B7280; font-size: 12px; cursor: pointer; text-align: center; transition: background 0.15s; } .csk-history-panel__loadmore:hover { background: #F3F4F6; } /* ========== 移动端适配 ========== */ @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); }); } /** XSS 转义 - 防止用户输入中的 HTML 注入 */ function escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }; return text.replace(/[&<>"']/g, (ch) => map[ch] || ch); } /** 防抖函数 */ 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'; // 历史会话按钮(P2) const historyBtn = document.createElement('button'); historyBtn.className = 'csk-history-btn'; historyBtn.setAttribute('title', t('history_title')); historyBtn.innerHTML = ``; // 最小化按钮 const minimizeBtn = document.createElement('button'); minimizeBtn.className = 'csk-header__btn csk-header__btn--minimize'; minimizeBtn.setAttribute('title', t('minimize')); 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', t('close')); closeBtn.innerHTML = ``; closeBtn.addEventListener('click', () => { windowEl.classList.add('csk-window--hidden'); }); actions.appendChild(historyBtn); 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'; // === 会话管理面板(P2,默认隐藏) === const historyPanel = document.createElement('div'); historyPanel.className = 'csk-history-panel csk-history-panel--hidden'; historyPanel.innerHTML = `
${escapedCode}`);
return `${CODE_BLOCK_PREFIX}${idx}\x00`;
});
// 2. 提取行内代码
const inlineCodes = [];
processed = processed.replace(/`([^`\n]+)`/g, (_match, code) => {
const idx = inlineCodes.length;
inlineCodes.push(`${escapeHtml(code)}`);
return `${INLINE_CODE_PREFIX}${idx}\x00`;
});
// 3. 转义剩余 HTML(代码块和行内代码已安全处理)
processed = escapeHtml(processed);
// 4. 还原代码块和行内代码占位符(它们已经是安全 HTML)
processed = restorePlaceholders(processed, CODE_BLOCK_PREFIX, codeBlocks);
processed = restorePlaceholders(processed, INLINE_CODE_PREFIX, inlineCodes);
// 5. 逐行处理 Markdown 语法
const lines = processed.split('\n');
const result = [];
let inList = false;
let listType = ''; // 'ul' 或 'ol'
let inBlockquote = false;
let paragraphBuffer = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 代码块已在占位符还原阶段处理,直接输出
if (line.includes(CODE_BLOCK_PREFIX) || line.includes('')) {
flushParagraph();
closeList();
closeBlockquote();
result.push(line);
continue;
}
// 标题
const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
if (headingMatch) {
flushParagraph();
closeList();
closeBlockquote();
const level = headingMatch[1].length;
result.push(`${headingMatch[2]} `);
continue;
}
// 引用
const quoteMatch = line.match(/^>\s?(.*)/);
if (quoteMatch) {
flushParagraph();
closeList();
if (!inBlockquote) {
inBlockquote = true;
result.push('');
}
result.push(`${inlineFormat(quoteMatch[1])}
`);
continue;
}
else if (inBlockquote) {
closeBlockquote();
}
// 无序列表
const ulMatch = line.match(/^[\-\*]\s+(.+)/);
if (ulMatch) {
flushParagraph();
closeBlockquote();
if (!inList || listType !== 'ul') {
closeList();
inList = true;
listType = 'ul';
result.push('');
}
result.push(`- ${inlineFormat(ulMatch[1])}
`);
continue;
}
// 有序列表
const olMatch = line.match(/^\d+\.\s+(.+)/);
if (olMatch) {
flushParagraph();
closeBlockquote();
if (!inList || listType !== 'ol') {
closeList();
inList = true;
listType = 'ol';
result.push('');
}
result.push(`- ${inlineFormat(olMatch[1])}
`);
continue;
}
// 空行 → 段落分隔
if (line.trim() === '') {
flushParagraph();
closeList();
continue;
}
// 水平线
if (/^(\*{3,}|-{3,}|_{3,})$/.test(line.trim())) {
flushParagraph();
closeList();
closeBlockquote();
result.push('
');
continue;
}
// 普通文本 → 收集到段落缓冲
closeList();
closeBlockquote();
paragraphBuffer.push(inlineFormat(line));
}
flushParagraph();
closeList();
closeBlockquote();
return result.join('\n');
// === 辅助函数 ===
/** 行内格式化:粗体、斜体、链接 */
function inlineFormat(text) {
// 粗体 **text** 或 __text__
text = text.replace(/\*\*(.+?)\*\*/g, '$1');
text = text.replace(/__(.+?)__/g, '$1');
// 斜体 *text* 或 _text_
text = text.replace(/\*(.+?)\*/g, '$1');
text = text.replace(/(?$1');
// 删除线 ~~text~~
text = text.replace(/~~(.+?)~~/g, '$1');
// 链接 [text](url)
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, linkText, url) => {
// 只允许 http/https 链接,防止 javascript: 协议
const safeUrl = /^https?:\/\//i.test(url) ? url : '#';
return `${linkText}`;
});
return text;
}
/** 将段落缓冲输出为 */
function flushParagraph() {
if (paragraphBuffer.length > 0) {
result.push(`
${paragraphBuffer.join('
')}
`);
paragraphBuffer = [];
}
}
/** 关闭列表 */
function closeList() {
if (inList) {
result.push(listType === 'ul' ? '
' : '');
inList = false;
listType = '';
}
}
/** 关闭引用块 */
function closeBlockquote() {
if (inBlockquote) {
result.push('
');
inBlockquote = false;
}
}
}
/** 还原占位符为安全 HTML */
function restorePlaceholders(text, prefix, replacements) {
return text.replace(new RegExp(escapeRegex(prefix) + '(\\d+)\x00', 'g'), (_m, idx) => {
return replacements[parseInt(idx)] || '';
});
}
/** 转义正则特殊字符 */
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
let config$1 = null;
let messages = [];
let messagesContainer$1 = null;
let inputEl$1 = null;
let sendBtn$1 = null;
let clearBtn$1 = null;
let categorySelect$1 = null;
let historyPanel$1 = null;
let showLoadingFn$1 = null;
let hideLoadingFn$1 = null;
let isSending = false;
/** 当前选中的知识库分类 ID */
let currentCategoryId;
/** 当前是否使用 RAG 对话 */
let useRag = false;
/**
* 初始化对话模块
*/
function initChat(cfg, dom) {
config$1 = cfg;
messagesContainer$1 = dom.messagesContainer;
inputEl$1 = dom.inputEl;
sendBtn$1 = dom.sendBtn;
clearBtn$1 = dom.clearBtn;
categorySelect$1 = dom.categorySelect;
historyPanel$1 = dom.historyPanel;
showLoadingFn$1 = dom.showLoading;
hideLoadingFn$1 = dom.hideLoading;
// 初始化知识库分类
currentCategoryId = cfg.categoryId;
useRag = !!cfg.categoryId || !!cfg.showCategorySwitch;
// 绑定发送事件
bindSendEvents();
// 加载知识库分类下拉框
if (cfg.showCategorySwitch && categorySelect$1) {
loadCategories();
}
}
/**
* 初始化 chatId 并加载对话历史
* 异步流程:查后端会话 → 恢复 chatId → 加载历史消息
*/
async function initChatHistory() {
if (!config$1 || !messagesContainer$1)
return;
// 1. 初始化 chatId(从后端获取已有会话或自动生成)
await initChatId();
// 2. 尝试从后端加载对话历史
await loadHistoryFromBackend();
// 3. 如果后端无历史,尝试从 localStorage 恢复
if (messages.length === 0) {
const cached = loadMessages(config$1.integrateId);
if (cached.length > 0) {
messages = cached;
renderHistory();
logger.info(`从本地缓存恢复 ${cached.length} 条消息`);
}
}
}
/**
* 从后端加载对话历史
*/
async function loadHistoryFromBackend() {
if (!config$1 || !messagesContainer$1)
return;
const chatId = getChatId();
if (!chatId)
return;
try {
const result = await fetchConversationMessages(chatId);
if (result.messages.length > 0) {
// 将后端消息转换为 ChatMessage 格式
messages = result.messages.map((msg, idx) => ({
id: uuid(),
role: msg.messageType === 'USER' ? 'user' : 'ai',
content: msg.content,
timestamp: new Date(msg.createTime).getTime(),
}));
renderHistory();
logger.info(`从后端加载 ${messages.length} 条历史消息`);
// 同步到 localStorage
saveMessages(config$1.integrateId, messages);
}
}
catch (err) {
logger.warn('从后端加载历史消息失败', err);
}
}
/** 绑定发送相关事件 */
function bindSendEvents() {
if (!inputEl$1 || !sendBtn$1)
return;
sendBtn$1.addEventListener('click', () => handleSend());
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();
inputEl$1.style.height = 'auto';
isSending = true;
updateSendBtnState();
// 确保 chatId 已初始化
if (!config$1.chatId) {
await initChatId();
}
// 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();
const shouldUseRag = useRag && (currentCategoryId !== undefined || config$1.categoryId !== undefined);
if (config$1.streaming) {
aiContent = await sendStreamMessage(text, aiTimestamp, shouldUseRag);
}
else {
aiContent = await chatRequest(text);
}
if (hideLoadingFn$1)
hideLoadingFn$1();
if (!config$1.streaming && messagesContainer$1) {
renderAIBubble(messagesContainer$1, aiContent, aiTimestamp, renderMarkdown);
}
const aiMsg = { id: uuid(), role: 'ai', content: aiContent, timestamp: aiTimestamp };
messages.push(aiMsg);
saveMessages(config$1.integrateId, messages);
if (messagesContainer$1)
scrollToBottom(messagesContainer$1);
// RAG 引用来源
if (shouldUseRag)
fetchAndRenderSources(text, aiMsg);
}
catch (err) {
if (hideLoadingFn$1)
hideLoadingFn$1();
const errMsg = err instanceof CskError ? err.message : t('error_send');
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, shouldUseRag) {
return new Promise((resolve, reject) => {
let bubbleEl = null;
let accumulated = '';
let streamStarted = false;
chatSSERequest(text, (chunk) => {
accumulated += chunk;
if (!streamStarted && messagesContainer$1) {
if (hideLoadingFn$1)
hideLoadingFn$1();
const { bubble } = createEmptyAIBubble(messagesContainer$1, aiTimestamp);
bubbleEl = bubble;
streamStarted = true;
}
if (bubbleEl)
bubbleEl.textContent = accumulated;
if (messagesContainer$1)
scrollToBottom(messagesContainer$1);
}, () => {
if (!streamStarted && accumulated === '') {
chatRequest(text).then(resolve).catch(reject);
return;
}
if (bubbleEl && accumulated)
bubbleEl.innerHTML = renderMarkdown(accumulated);
resolve(accumulated);
}, (error) => {
if (accumulated.length > 0) {
if (bubbleEl)
bubbleEl.innerHTML = renderMarkdown(accumulated + '\n\n' + t('stream_interrupted'));
resolve(accumulated);
}
else {
reject(error);
}
}, currentCategoryId, shouldUseRag);
});
}
/** 获取并渲染 RAG 引用来源 */
async function fetchAndRenderSources(message, aiMsg) {
try {
const sources = await fetchRagSources(message, currentCategoryId);
if (sources.length > 0) {
const ragSources = sources.map(s => {
var _a, _b;
return ({
documentId: s.documentId || '',
title: s.title || '',
sourceName: s.sourceName || '',
chunkIndex: (_a = s.chunkIndex) !== null && _a !== void 0 ? _a : 0,
score: (_b = s.score) !== null && _b !== void 0 ? _b : 0,
snippet: s.snippet || '',
});
});
aiMsg.sources = ragSources;
if (messagesContainer$1) {
const lastAiMsg = messagesContainer$1.querySelector('.csk-msg--ai:last-of-type');
if (lastAiMsg)
renderSources(lastAiMsg, ragSources);
}
if (config$1)
saveMessages(config$1.integrateId, messages);
}
}
catch (err) {
logger.warn('获取引用来源失败', err);
}
}
/** 加载知识库分类到下拉框 */
async function loadCategories() {
if (!categorySelect$1)
return;
try {
const tree = await fetchCategoryTree();
if (tree.length === 0)
return;
categorySelect$1.innerHTML = ``;
const addOptions = (nodes, indent = 0) => {
for (const node of nodes) {
const option = document.createElement('option');
option.value = String(node.id);
option.textContent = `${' '.repeat(indent)}${node.name}`;
if (currentCategoryId !== undefined && String(node.id) === String(currentCategoryId))
option.selected = true;
categorySelect$1.appendChild(option);
if (node.children && node.children.length > 0)
addOptions(node.children, indent + 1);
}
};
addOptions(tree);
logger.info(`知识库分类加载成功 count=${tree.length}`);
}
catch (err) {
logger.error(t('category_load_error'), err);
}
}
/** 渲染历史消息 */
function renderHistory() {
if (!messagesContainer$1)
return;
const historyPanelEl = messagesContainer$1.querySelector('.csk-history-panel');
const msgs = messagesContainer$1.querySelectorAll('.csk-msg, .csk-loading');
msgs.forEach(el => el.remove());
for (const msg of messages) {
if (msg.role === 'user') {
renderUserBubble(messagesContainer$1, msg.content, msg.timestamp);
}
else {
const wrapper = renderAIBubble(messagesContainer$1, msg.content, msg.timestamp, renderMarkdown);
if (msg.sources && msg.sources.length > 0)
renderSources(wrapper, msg.sources);
}
}
scrollToBottom(messagesContainer$1);
if (clearBtn$1 && messages.length > 0)
clearBtn$1.style.display = 'inline-flex';
if (historyPanelEl && !messagesContainer$1.contains(historyPanelEl)) {
messagesContainer$1.appendChild(historyPanelEl);
}
}
/** 清空对话历史(生成新 chatId) */
function handleClear() {
if (!config$1)
return;
if (!confirm(t('clear_confirm')))
return;
messages = [];
if (messagesContainer$1) {
const msgs = messagesContainer$1.querySelectorAll('.csk-msg, .csk-loading');
msgs.forEach(el => el.remove());
}
if (clearBtn$1)
clearBtn$1.style.display = 'none';
clearMessages(config$1.integrateId);
// 生成新的 chatId,开始新会话
const newId = generateNewChatId();
updateChatId(newId);
saveCachedChatId(config$1.integrateId, config$1.userId, newId);
logger.lifecycleClear(config$1.integrateId);
logger.info(`新 chatId=${newId}`);
}
/** 生成新 chatId */
function generateNewChatId() {
const random = typeof crypto !== 'undefined' && crypto.randomUUID
? crypto.randomUUID().substring(0, 8)
: Math.random().toString(36).substring(2, 10);
return `sdk_${Date.now()}_${random}`;
}
/** 设置当前知识库分类 */
function setCategory(categoryId) {
currentCategoryId = categoryId;
useRag = categoryId !== undefined;
logger.lifecycleCategoryChange(categoryId !== null && categoryId !== void 0 ? categoryId : '全部');
}
// ==================== 会话管理面板 ====================
/** 加载会话列表并渲染 */
async function loadHistoryConversations() {
if (!historyPanel$1 || !config$1)
return;
const listEl = historyPanel$1.querySelector('#csk-history-list');
if (!listEl)
return;
listEl.innerHTML = `加载中...`;
try {
const result = await fetchConversationList(1, 50, config$1.userId, config$1.integrateId);
const items = result.list.map(c => ({
id: c.conversationId || c.chatId || '',
chatId: c.conversationId || c.chatId || '',
messageCount: c.messageCount,
lastMessageTime: c.lastMessageTime,
createdAt: c.firstMessageTime || c.createdAt,
}));
renderHistoryList(listEl, items, (id) => { window.open(getConversationExportUrl(id), '_blank'); }, async (id) => {
if (!confirm(t('history_delete_confirm')))
return;
const ok = await deleteConversation(id);
if (ok)
loadHistoryConversations();
});
}
catch (err) {
logger.error(t('history_load_error'), err);
listEl.innerHTML = `${t('history_load_error')}`;
}
}
// ==================== 单例状态 ====================
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 categorySelect = null;
let historyPanel = 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;
config = parsed;
// 2. 设置国际化语言
setLocale(config.locale);
// 3. 设置日志级别
setDebug(config.debug);
// 4. 设置 API 配置
setApiConfig(config);
// 5. 注入样式
injectStyles(config);
// 6. 创建悬浮按钮
launcherEl = createLauncher(config, toggle);
document.body.appendChild(launcherEl);
// 7. 创建聊天弹窗
const dom = createChatWindow(config);
windowEl = dom.window;
messagesContainer = dom.messagesContainer;
inputEl = dom.inputEl;
sendBtn = dom.sendBtn;
clearBtn = dom.clearBtn;
categorySelect = dom.categorySelect;
historyPanel = dom.historyPanel;
showLoadingFn = dom.showLoading;
hideLoadingFn = dom.hideLoading;
document.body.appendChild(windowEl);
// 8. 启用拖拽
const headerEl = windowEl.querySelector('.csk-header');
if (headerEl) {
dragCleanup = enableDrag(headerEl, windowEl);
}
// 9. 初始化对话模块
initChat(config, {
messagesContainer,
inputEl,
sendBtn,
clearBtn,
categorySelect,
historyPanel,
showLoading: showLoadingFn,
hideLoading: hideLoadingFn,
});
// 10. 监听知识库分类切换事件
windowEl.addEventListener('csk:categoryChange', ((e) => {
setCategory(e.detail.categoryId);
}));
// 11. 监听会话管理面板加载事件
windowEl.addEventListener('csk:loadHistory', () => {
loadHistoryConversations();
});
isInitialized = true;
logger.lifecycleInit(config.integrateId, config.requestDomain);
// 12. 异步初始化 chatId 和对话历史(不阻塞 UI)
initChatHistory().catch(err => {
logger.warn('chatId 初始化失败,将在发送消息时重试', err);
});
}
/** 销毁 SDK 实例 */
function destroy() {
if (!isInitialized)
return;
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;
categorySelect = null;
historyPanel = null;
showLoadingFn = null;
hideLoadingFn = null;
logger.lifecycleDestroy(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;
if (clearBtn) {
clearBtn.click();
}
else if (confirm('确定清空所有对话记录?')) {
clearMessages(config.integrateId);
}
}
// ==================== 挂载到全局 ====================
const ChatbotSDK = {
init,
destroy,
open,
close,
toggle,
clearHistory,
};
if (typeof window !== 'undefined') {
window.ChatbotSDK = ChatbotSDK;
}
return ChatbotSDK;
})();
//# sourceMappingURL=chatbot-sdk.js.map