var ChatbotSDK = (function () { 'use strict'; const PREFIX = '[ChatbotSDK]'; let debugEnabled = true; /** 错误回调函数(由宿主通过 onError 配置注入) */ let errorCallback = null; /** 设置是否开启调试日志 */ function setDebug(enabled) { debugEnabled = enabled; } /** 设置错误回调(宿主 onError 配置) */ function setErrorCallback(cb) { errorCallback = cb || null; } /** 性能计时器 */ 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 开关控制;同时触发 onError 回调) */ error(msg, data) { console.error(PREFIX, msg, data !== undefined ? data : ''); // 触发宿主的 onError 回调 if (errorCallback) { try { const code = data instanceof Error ? data.type || 'error' : 'error'; errorCallback({ message: msg, code: String(code), detail: data }); } catch ( /* 回调异常不影响 SDK */_a) { /* 回调异常不影响 SDK */ } } }, /** 开始计时 */ 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 = ` `; // ==================== 马卡龙色系玻璃质感图标 ==================== /** * 生成玻璃质感机器人 SVG 图标 * 包含:圆润机器人头部 + 眨眼动效 + 跳动气泡 + 高光反射层 */ function buildGlassIcon(eyeColor, smileColor, bubbleColor) { return ` `; } /** 梦幻紫粉主题图标 */ const GLASS_ICON_DREAM_PURPLE = buildGlassIcon('rgba(139,92,246,0.6)', // 眼睛:紫色 'rgba(139,92,246,0.45)', // 微笑 'rgba(240,171,252,0.85)' // 气泡:粉紫 ); /** 薄荷青绿主题图标 */ const GLASS_ICON_MINT_TECH = buildGlassIcon('rgba(16,185,129,0.6)', // 眼睛:翠绿 'rgba(16,185,129,0.45)', // 微笑 'rgba(110,231,183,0.85)' // 气泡:薄荷 ); /** 珊瑚蜜桃主题图标 */ const GLASS_ICON_CORAL_PEACH = buildGlassIcon('rgba(244,63,94,0.55)', // 眼睛:珊瑚红 'rgba(244,63,94,0.4)', // 微笑 'rgba(253,186,116,0.85)' // 气泡:蜜桃 ); /** 天空蓝紫主题图标 */ const GLASS_ICON_SKY_BLUE = buildGlassIcon('rgba(56,189,248,0.6)', // 眼睛:天蓝 'rgba(56,189,248,0.45)', // 微笑 'rgba(167,139,250,0.85)' // 气泡:淡紫 ); /** 主题 → 默认主色映射(当用户未指定 primaryColor 时自动使用) */ const THEME_PRIMARY_COLORS = { 'dream-purple': '#A78BFA', 'mint-tech': '#34D399', 'coral-peach': '#FB7185', 'sky-blue': '#7DD3FC', }; /** * 解析并校验用户传入的配置,填充默认值 */ function parseConfig(raw) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; // 校验必传参数: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 (_m) { logger.error(`requestDomain 不是合法的 URL 格式:${raw.requestDomain}。请提供完整的域名,如 https://api.example.com`); return null; } // integrateId 统一转为字符串(后端 roleId 为 Long,但 query param 传字符串也可接收) const integrateIdStr = String(raw.integrateId).trim(); // 解析 launcherTheme:根据主题自动选择图标和默认主色 const launcherTheme = raw.launcherTheme || undefined; let resolvedLauncherIcon = raw.launcherIcon || DEFAULT_LAUNCHER_ICON; let resolvedPrimaryColor = raw.primaryColor || '#4F46E5'; if (launcherTheme && !raw.launcherIcon) { // 有主题且未自定义图标 → 使用主题专属玻璃质感图标 const themeIconsMap = { 'dream-purple': GLASS_ICON_DREAM_PURPLE, 'mint-tech': GLASS_ICON_MINT_TECH, 'coral-peach': GLASS_ICON_CORAL_PEACH, 'sky-blue': GLASS_ICON_SKY_BLUE, }; resolvedLauncherIcon = themeIconsMap[launcherTheme] || DEFAULT_LAUNCHER_ICON; // 若未自定义主色 → 使用主题默认色 if (!raw.primaryColor) { resolvedPrimaryColor = THEME_PRIMARY_COLORS[launcherTheme] || '#4F46E5'; } } // 填充默认值 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, height: Math.max(300, (_c = raw.height) !== null && _c !== void 0 ? _c : 520), position: raw.position === 'left-bottom' ? 'left-bottom' : 'right-bottom', primaryColor: resolvedPrimaryColor, launcherTheme: launcherTheme, launcherIcon: resolvedLauncherIcon, showClear: (_d = raw.showClear) !== null && _d !== void 0 ? _d : true, showAdminPanel: (_e = raw.showAdminPanel) !== null && _e !== void 0 ? _e : false, quickReplies: Array.isArray(raw.quickReplies) ? raw.quickReplies.map(s => String(s).trim()).filter(Boolean) : [], theme: raw.theme === 'dark' ? 'dark' : 'light', showTeaser: (_f = raw.showTeaser) !== null && _f !== void 0 ? _f : true, teaserText: (typeof raw.teaserText === 'string' && raw.teaserText.trim()) || '', streaming: (_g = raw.streaming) !== null && _g !== void 0 ? _g : true, enableRag: (_h = raw.enableRag) !== null && _h !== void 0 ? _h : true, rewriteStrategy: raw.rewriteStrategy || 'REWRITE', locale: raw.locale || 'zh-CN', debug: (_j = raw.debug) !== null && _j !== void 0 ? _j : true, sound: (_k = raw.sound) !== null && _k !== void 0 ? _k : false, notification: (_l = raw.notification) !== null && _l !== void 0 ? _l : false, onError: typeof raw.onError === 'function' ? raw.onError : undefined, onReady: typeof raw.onReady === 'function' ? raw.onReady : undefined, onMessage: typeof raw.onMessage === 'function' ? raw.onMessage : undefined, 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: '关闭', status_online: '在线', // 欢迎空状态 welcome_title: '你好,我是 AI 智能助手', welcome_desc: '有什么可以帮你的吗?在下方输入框开始提问吧~', // 输入区 placeholder: '输入您的问题...', send: '发送', stop: '停止生成', new_message: '新消息', // 消息操作 copy: '复制', copied: '已复制', retry: '重新生成', retrying: '重新生成中...', // 消息 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: '删除', history_search: '搜索会话...', history_group_today: '今天', history_group_yesterday: '昨天', history_group_week: '本周', history_group_earlier: '更早', // 消息反馈 feedback_up: '有帮助', feedback_down: '没帮助', // 提示气泡 teaser_text: '有什么可以帮你的吗?', new_msg_announce: '收到新消息', // 错误提示 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', status_online: 'Online', // Welcome welcome_title: 'Hi, I am your AI assistant', welcome_desc: 'How can I help you? Type your question below to get started.', // Input placeholder: 'Type your question...', send: 'Send', stop: 'Stop generating', new_message: 'New message', // Message actions copy: 'Copy', copied: 'Copied', retry: 'Regenerate', retrying: 'Regenerating...', // 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', history_search: 'Search conversations...', history_group_today: 'Today', history_group_yesterday: 'Yesterday', history_group_week: 'This Week', history_group_earlier: 'Earlier', // Feedback feedback_up: 'Helpful', feedback_down: 'Not helpful', // Teaser teaser_text: 'How can I help you?', new_msg_announce: 'New message received', // 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', currentConfig.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', currentConfig.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 封装,支持外部 AbortSignal(用于流式主动中断) */ async function safeFetch(url, options = {}, timeout = REQUEST_TIMEOUT, externalSignal) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); // 外部信号触发时同步中断内部请求 if (externalSignal) { if (externalSignal.aborted) { controller.abort(); } else { externalSignal.addEventListener('abort', () => controller.abort(), { once: true }); } } try { const response = await fetch(url, Object.assign(Object.assign({}, options), { signal: controller.signal, mode: 'cors', credentials: 'include' })); return response; } catch (err) { // 用户主动中断(停止生成),不算错误 if (externalSignal === null || externalSignal === void 0 ? void 0 : externalSignal.aborted) { throw new CskError('aborted', 'aborted'); } 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, signal) { 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, signal); 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 (signal === null || signal === void 0 ? void 0 : signal.aborted) { logger.info(`流式被用户中断,保留已生成内容 length=${totalText.length}`); } else 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) { // 用户主动中断不触发 onError,走 onDone if ((signal === null || signal === void 0 ? void 0 : signal.aborted) || (err instanceof CskError && err.type === 'aborted')) { onDone(); return; } 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 []; } } // ==================== P0-002: 消息反馈 ==================== /** * 提交消息反馈(点赞/点踩) */ async function submitFeedbackApi(messageId, feedbackType) { if (!currentConfig) return false; const url = buildUrl('/feedback'); try { const response = await safeFetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messageId: String(messageId), conversationId: currentConfig.chatId, feedbackType, }), }); if (!response.ok) { logger.error(`反馈提交失败 status=${response.status}`); return false; } const json = await response.json(); return json.success || false; } catch (err) { logger.error('反馈提交异常', err); return false; } } /** * 获取会话列表 */ 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; /** 将 hex 颜色解析为 "r, g, b" 字符串,用于 rgba() 拼接 */ function hexToRgb(hex) { const match = hex.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/); if (!match) return '79, 70, 229'; return `${parseInt(match[1], 16)}, ${parseInt(match[2], 16)}, ${parseInt(match[3], 16)}`; } /** CSS 变量:将配置中的主题色转换为 CSS 自定义属性 */ function cssVars(config) { const darker = adjustColor(config.primaryColor, -15); const lighter = adjustColor(config.primaryColor, 18); const rgb = hexToRgb(config.primaryColor); // 马卡龙主题渐变色映射 const themeGradients = { 'dream-purple': { grad1: '#A78BFA', grad2: '#F0ABFC', glow: '167, 139, 250' }, 'mint-tech': { grad1: '#6EE7B7', grad2: '#34D399', glow: '52, 211, 153' }, 'coral-peach': { grad1: '#FB7185', grad2: '#FDBA74', glow: '251, 113, 133' }, 'sky-blue': { grad1: '#7DD3FC', grad2: '#A78BFA', glow: '125, 211, 252' }, }; const themeColors = config.launcherTheme ? themeGradients[config.launcherTheme] : null; return ` --csk-primary: ${config.primaryColor}; --csk-primary-hover: ${darker}; --csk-primary-light: ${lighter}; --csk-primary-rgb: ${rgb}; --csk-bg-user: linear-gradient(135deg, ${config.primaryColor}, ${darker}); --csk-bg-ai: #ffffff; --csk-text-user: #ffffff; --csk-text-ai: #1F2937; --csk-window-width: ${config.width}px; --csk-window-height: ${config.height}px; --csk-radius: 16px; --csk-shadow-window: 0 12px 48px rgba(15, 23, 42, 0.18), 0 2px 8px rgba(15, 23, 42, 0.06); --csk-shadow-bubble: 0 1px 2px rgba(15, 23, 42, 0.06); --csk-border: #ECEEF2; --csk-bg-app: #F6F7F9; ${themeColors ? ` --csk-launcher-grad-1: ${themeColors.grad1}; --csk-launcher-grad-2: ${themeColors.grad2}; --csk-launcher-glow: ${themeColors.glow}; ` : ''} `; } /** 简单的颜色加深(按通道加减) */ function adjustColor(hex, amount) { 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", "PingFang SC", "Microsoft YaHei", sans-serif; font-size: 14px; line-height: 1.55; color: #1F2937; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; box-sizing: border-box; } .csk-root *, .csk-root *::before, .csk-root *::after { box-sizing: border-box; } /* ========== 悬浮按钮 ========== */ .csk-launcher { position: fixed; bottom: 24px; z-index: 9998; width: 60px; height: 60px; border-radius: 50%; background: var(--csk-bg-user); display: flex; align-items: center; justify-content: center; cursor: pointer; color: #fff; user-select: none; overflow: visible; /* 半透明白色光环边框,在白色页面上清晰分离按钮与背景 */ border: 2.5px solid rgba(255, 255, 255, 0.55); /* 三层投影:接地阴影 + 彩色光晕,把按钮从白底「托」出来 */ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06), 0 4px 16px rgba(0, 0, 0, 0.10), 0 6px 24px rgba(var(--csk-primary-rgb), 0.28), 0 0 48px rgba(var(--csk-primary-rgb), 0.12); transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.25s ease, border-color 0.25s ease; animation: csk-launcher-in 0.55s cubic-bezier(0.22, 1, 0.36, 1) both; } .csk-launcher--right { right: 24px; } .csk-launcher--left { left: 24px; } /* 高光扫过动效 */ .csk-launcher::before { content: ''; position: absolute; top: -50%; left: -50%; width: 40%; height: 200%; border-radius: 50%; background: linear-gradient( 105deg, transparent 30%, rgba(255, 255, 255, 0.28) 48%, rgba(255, 255, 255, 0.1) 52%, transparent 70% ); transform: rotate(25deg); animation: csk-shimmer 4s ease-in-out infinite; pointer-events: none; } /* 呼吸色晕 — 配合接地阴影,强化白底上的立体存在感 */ .csk-launcher::after { content: ''; position: absolute; inset: -18px; border-radius: 50%; background: radial-gradient(circle, rgba(var(--csk-primary-rgb), 0.18) 0%, rgba(var(--csk-primary-rgb), 0.05) 45%, transparent 68%); animation: csk-breathe 3.5s ease-in-out infinite; pointer-events: none; } .csk-launcher:hover { transform: translateY(-3px) scale(1.08); border-color: rgba(255, 255, 255, 0.75); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.10), 0 8px 24px rgba(0, 0, 0, 0.14), 0 10px 36px rgba(var(--csk-primary-rgb), 0.32), 0 0 56px rgba(var(--csk-primary-rgb), 0.14); } .csk-launcher:hover::before { animation-play-state: paused; } .csk-launcher:hover::after { animation: none; inset: -22px; background: radial-gradient(circle, rgba(var(--csk-primary-rgb), 0.22) 0%, rgba(var(--csk-primary-rgb), 0.06) 45%, transparent 68%); } .csk-launcher:active { transform: scale(0.93); } .csk-launcher svg { width: 30px; height: 30px; position: relative; z-index: 1; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.15)); } @keyframes csk-launcher-in { 0% { opacity: 0; transform: scale(0.5) translateY(16px); } 60% { transform: scale(1.06) translateY(-2px); } 100% { opacity: 1; transform: scale(1) translateY(0); } } @keyframes csk-shimmer { 0%, 100% { left: -60%; opacity: 0; } 20% { opacity: 1; } 60% { opacity: 1; } 80% { left: 120%; opacity: 0; } } @keyframes csk-breathe { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.15); opacity: 0.35; } } /* SVG 图标内部动效:眨眼 */ .csk-launcher .csk-ico-eye { animation: csk-eye-blink 4s ease-in-out infinite; } @keyframes csk-eye-blink { 0%, 42%, 48%, 100% { opacity: 1; } 45% { opacity: 0.1; } } /* SVG 图标内部动效:天线能量闪烁 */ .csk-launcher .csk-ico-antenna { animation: csk-antenna-glow 2s ease-in-out infinite; } @keyframes csk-antenna-glow { 0%, 100% { opacity: 0.5; } 50% { opacity: 1; } } /* ========== 磨砂玻璃质感悬浮按钮 ========== */ .csk-launcher--glass { background: linear-gradient( 135deg, var(--csk-launcher-grad-1, var(--csk-primary)) 0%, var(--csk-launcher-grad-2, var(--csk-primary-light)) 100% ) !important; backdrop-filter: blur(12px) saturate(180%); -webkit-backdrop-filter: blur(12px) saturate(180%); border: 1.5px solid rgba(255, 255, 255, 0.55) !important; /* 多层阴影:接地 + 彩色光晕 + 内阴影立体感 */ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04), 0 6px 20px rgba(0, 0, 0, 0.08), 0 8px 32px rgba(var(--csk-launcher-glow, var(--csk-primary-rgb)), 0.30), inset 0 1px 1px rgba(255, 255, 255, 0.35), inset 0 -1px 1px rgba(0, 0, 0, 0.04) !important; } .csk-launcher--glass:hover { border-color: rgba(255, 255, 255, 0.75) !important; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.06), 0 10px 28px rgba(0, 0, 0, 0.10), 0 12px 40px rgba(var(--csk-launcher-glow, var(--csk-primary-rgb)), 0.38), inset 0 1px 2px rgba(255, 255, 255, 0.45), inset 0 -1px 1px rgba(0, 0, 0, 0.04) !important; } /* 玻璃按钮的呼吸色晕 — 更鲜艳、更醒目 */ .csk-launcher--glass::after { background: radial-gradient( circle, rgba(var(--csk-launcher-glow, var(--csk-primary-rgb)), 0.28) 0%, rgba(var(--csk-launcher-glow, var(--csk-primary-rgb)), 0.10) 40%, transparent 65% ) !important; animation: csk-breathe-glow 3.5s ease-in-out infinite; } @keyframes csk-breathe-glow { 0%, 100% { transform: scale(1); opacity: 0.8; } 50% { transform: scale(1.22); opacity: 0.25; } } /* 玻璃按钮的高光扫过 — 更柔和的玻璃质感 */ .csk-launcher--glass::before { background: linear-gradient( 115deg, transparent 25%, rgba(255, 255, 255, 0.35) 45%, rgba(255, 255, 255, 0.15) 55%, transparent 75% ) !important; width: 50%; animation: csk-shimmer-glass 5s ease-in-out infinite; } @keyframes csk-shimmer-glass { 0%, 100% { left: -70%; opacity: 0; } 15% { opacity: 1; } 55% { opacity: 1; } 70% { left: 130%; opacity: 0; } } /* SVG 内部动效:跳动气泡(3 个小圆点交错弹跳) */ .csk-launcher .csk-ico-bubble { animation: csk-bubble-jump 1.4s ease-in-out infinite; } .csk-launcher .csk-ico-bubble--1 { animation-delay: 0s; } .csk-launcher .csk-ico-bubble--2 { animation-delay: 0.18s; } .csk-launcher .csk-ico-bubble--3 { animation-delay: 0.36s; } @keyframes csk-bubble-jump { 0%, 100% { transform: translateY(0); } 40% { transform: translateY(-3.5px); } 60% { transform: translateY(-1px); } } /* ========== 聊天弹窗 ========== */ .csk-window { position: fixed; bottom: 24px; z-index: 9999; width: var(--csk-window-width); height: var(--csk-window-height); max-height: calc(100vh - 48px); background: #fff; border-radius: var(--csk-radius); box-shadow: var(--csk-shadow-window); display: flex; flex-direction: column; overflow: hidden; transform-origin: bottom right; transition: opacity 0.24s ease, transform 0.24s cubic-bezier(0.34, 1.2, 0.64, 1), visibility 0.24s; opacity: 1; transform: translateY(0) scale(1); visibility: visible; pointer-events: auto; } .csk-window--right { right: 24px; transform-origin: bottom right; } .csk-window--left { left: 24px; transform-origin: bottom left; } .csk-window--hidden { opacity: 0; transform: translateY(12px) scale(0.96); visibility: hidden; pointer-events: none; } /* ========== 头部 ========== */ .csk-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; min-height: 60px; background: var(--csk-bg-user); color: #fff; cursor: move; user-select: none; position: relative; flex-shrink: 0; } .csk-header::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 24px; background: linear-gradient(to bottom, rgba(0,0,0,0.04), transparent); pointer-events: none; } .csk-header__left { display: flex; align-items: center; gap: 12px; min-width: 0; } .csk-header__avatar { width: 38px; height: 38px; border-radius: 50%; background: rgba(255, 255, 255, 0.22); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; flex-shrink: 0; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.25); } .csk-header__avatar svg { width: 22px; height: 22px; } .csk-header__info { display: flex; flex-direction: column; min-width: 0; } .csk-header__title { font-size: 15px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; letter-spacing: 0.2px; } .csk-header__status { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; opacity: 0.9; margin-top: 2px; } .csk-status-dot { width: 7px; height: 7px; border-radius: 50%; background: #6EE7B7; box-shadow: 0 0 0 0 rgba(110, 231, 183, 0.7); animation: csk-status-pulse 2s infinite; } @keyframes csk-status-pulse { 0% { box-shadow: 0 0 0 0 rgba(110, 231, 183, 0.7); } 70% { box-shadow: 0 0 0 6px rgba(110, 231, 183, 0); } 100% { box-shadow: 0 0 0 0 rgba(110, 231, 183, 0); } } .csk-header__actions { display: flex; align-items: center; gap: 2px; flex-shrink: 0; } .csk-header__btn, .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: 8px; transition: background 0.15s, transform 0.15s; } .csk-header__btn:hover, .csk-history-btn:hover { background: rgba(255, 255, 255, 0.2); } .csk-header__btn:active, .csk-history-btn:active { transform: scale(0.92); } /* ========== 消息区 ========== */ .csk-messages { flex: 1; overflow-y: auto; padding: 20px 16px 12px; background: var(--csk-bg-app); scroll-behavior: smooth; } .csk-messages::-webkit-scrollbar { width: 6px; } .csk-messages::-webkit-scrollbar-track { background: transparent; } .csk-messages::-webkit-scrollbar-thumb { background: #D8DCE3; border-radius: 3px; } .csk-messages::-webkit-scrollbar-thumb:hover { background: #C2C7D0; } /* 欢迎空状态 */ .csk-welcome { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 48px 24px 32px; color: #6B7280; animation: csk-fade-in 0.3s ease; } .csk-welcome__avatar { width: 56px; height: 56px; border-radius: 50%; background: var(--csk-bg-user); color: #fff; display: flex; align-items: center; justify-content: center; margin-bottom: 14px; box-shadow: 0 6px 18px rgba(var(--csk-primary-rgb), 0.3); } .csk-welcome__avatar svg { width: 30px; height: 30px; } .csk-welcome__title { font-size: 15px; font-weight: 600; color: #1F2937; margin-bottom: 4px; } .csk-welcome__desc { font-size: 13px; color: #9CA3AF; line-height: 1.6; } /* 消息气泡 */ .csk-msg { display: flex; margin-bottom: 18px; max-width: 100%; word-break: break-word; animation: csk-msg-in 0.28s cubic-bezier(0.22, 1, 0.36, 1); } .csk-msg--user { flex-direction: row-reverse; align-items: flex-end; } .csk-msg--ai { flex-direction: row; align-items: flex-start; } .csk-msg__avatar { width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin: 0 8px; box-shadow: var(--csk-shadow-bubble); } .csk-msg__avatar svg { width: 18px; height: 18px; } .csk-msg__avatar--ai { background: #fff; color: var(--csk-primary); border: 1px solid var(--csk-border); } .csk-msg__avatar--user { background: var(--csk-bg-user); color: #fff; } .csk-msg__content { display: flex; flex-direction: column; min-width: 0; max-width: calc(100% - 46px); } .csk-msg--user .csk-msg__content { align-items: flex-end; } .csk-msg--ai .csk-msg__content { align-items: flex-start; } .csk-msg__bubble { padding: 10px 14px; border-radius: 16px; font-size: 14px; line-height: 1.6; box-shadow: var(--csk-shadow-bubble); } .csk-msg--user .csk-msg__bubble { background: var(--csk-bg-user); color: var(--csk-text-user); border-radius: 16px 16px 4px 16px; } .csk-msg--ai .csk-msg__bubble { background: var(--csk-bg-ai); color: var(--csk-text-ai); border-radius: 16px 16px 16px 4px; border: 1px solid var(--csk-border); } .csk-msg__time { font-size: 11px; color: #9CA3AF; margin-top: 5px; padding: 0 4px; } @keyframes csk-msg-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } @keyframes csk-fade-in { from { opacity: 0; } to { opacity: 1; } } /* ========== Loading 动画 ========== */ .csk-loading { display: flex; align-items: center; margin-bottom: 18px; animation: csk-msg-in 0.28s ease; } .csk-loading__avatar { width: 30px; height: 30px; border-radius: 50%; background: #fff; color: var(--csk-primary); border: 1px solid var(--csk-border); display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin: 0 8px; box-shadow: var(--csk-shadow-bubble); } .csk-loading__avatar svg { width: 18px; height: 18px; } .csk-loading__bubble { background: #fff; border: 1px solid var(--csk-border); border-radius: 16px 16px 16px 4px; padding: 12px 16px; display: flex; align-items: center; gap: 5px; } .csk-loading__dot { width: 7px; height: 7px; border-radius: 50%; background: #B4B9C2; 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); opacity: 0.6; } 40% { transform: scale(1); opacity: 1; } } /* ========== 输入区 ========== */ .csk-input-area { padding: 10px 12px 14px; background: #fff; border-top: 1px solid var(--csk-border); flex-shrink: 0; } .csk-input-wrap { display: flex; align-items: flex-end; gap: 8px; background: var(--csk-bg-app); border: 1px solid var(--csk-border); border-radius: 14px; padding: 6px 6px 6px 14px; transition: border-color 0.2s, box-shadow 0.2s, background 0.2s; } .csk-input-wrap--focus { border-color: var(--csk-primary); background: #fff; box-shadow: 0 0 0 3px rgba(var(--csk-primary-rgb), 0.12); } .csk-input { flex: 1; border: none; background: transparent; padding: 8px 0; font-size: 14px; outline: none; font-family: inherit; resize: none; min-height: 22px; max-height: 120px; line-height: 1.5; color: #1F2937; } .csk-input::placeholder { color: #9CA3AF; } .csk-root .csk-input:focus-visible { outline: none; } .csk-send-btn { display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; min-width: 36px; border: none; border-radius: 10px; background: var(--csk-bg-user); color: #fff; cursor: pointer; transition: transform 0.18s, box-shadow 0.18s, opacity 0.18s; box-shadow: 0 3px 10px rgba(var(--csk-primary-rgb), 0.3); } .csk-send-btn:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 5px 14px rgba(var(--csk-primary-rgb), 0.4); } .csk-send-btn:active:not(:disabled) { transform: scale(0.92); } .csk-send-btn:disabled { background: #D1D5DB; box-shadow: none; cursor: not-allowed; opacity: 0.7; } .csk-send-btn svg { width: 18px; height: 18px; } /* ========== 清空按钮 ========== */ .csk-clear-btn { display: inline-flex; align-items: center; gap: 5px; padding: 5px 12px; border: 1px solid var(--csk-border); border-radius: 999px; background: #fff; color: #6B7280; font-size: 12px; cursor: pointer; margin: 0 auto 8px; transition: all 0.15s; font-family: inherit; } .csk-clear-btn:hover { background: #FEF2F2; border-color: #FCA5A5; color: #DC2626; } /* ========== P1: 知识库分类下拉 ========== */ .csk-category-bar { display: flex; align-items: center; padding: 8px 12px; border-top: 1px solid var(--csk-border); background: #FBFBFC; gap: 8px; flex-shrink: 0; } .csk-category-bar__label { font-size: 13px; white-space: nowrap; } .csk-category-select { flex: 1; padding: 6px 28px 6px 10px; border: 1px solid var(--csk-border); border-radius: 8px; font-size: 12.5px; color: #374151; background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239CA3AF' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E") no-repeat right 9px center; appearance: none; -webkit-appearance: none; outline: none; cursor: pointer; font-family: inherit; transition: border-color 0.2s, box-shadow 0.2s; max-width: 220px; } .csk-category-select:focus { border-color: var(--csk-primary); box-shadow: 0 0 0 3px rgba(var(--csk-primary-rgb), 0.12); } /* ========== P1: RAG 引用来源卡片 ========== */ .csk-sources { margin-top: 8px; border: 1px solid var(--csk-border); border-radius: 12px; overflow: hidden; font-size: 12px; max-width: 100%; background: #FAFBFC; } .csk-sources__header { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: #F3F4F7; cursor: pointer; user-select: none; transition: background 0.15s; } .csk-sources__header:hover { background: #ECEEF2; } .csk-sources__title { display: flex; align-items: center; gap: 5px; font-weight: 500; color: #374151; } .csk-sources__arrow { transition: transform 0.2s; color: #9CA3AF; font-size: 10px; } .csk-sources--collapsed .csk-sources__arrow { transform: rotate(-90deg); } .csk-sources__body { border-top: 1px solid var(--csk-border); padding: 0; } .csk-sources--collapsed .csk-sources__body { display: none; } .csk-source-item { padding: 9px 12px; border-bottom: 1px solid #F0F1F4; transition: background 0.15s; } .csk-source-item:last-child { border-bottom: none; } .csk-source-item:hover { background: #fff; } .csk-source-item__name { font-weight: 500; color: #1F2937; margin-bottom: 3px; } .csk-source-item__snippet { color: #6B7280; line-height: 1.5; 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: 4px; } /* ========== 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: 14px 0 6px; font-weight: 600; line-height: 1.3; color: #111827; } .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: 10px; overflow-x: auto; margin: 8px 0; font-size: 13px; line-height: 1.55; font-family: 'SF Mono', 'Consolas', 'Menlo', 'Monaco', monospace; } .csk-md-code-block code { background: none; padding: 0; border-radius: 0; font-size: inherit; color: inherit; } .csk-md-inline-code { background: rgba(var(--csk-primary-rgb), 0.1); color: var(--csk-primary); padding: 1px 6px; border-radius: 5px; font-size: 13px; font-family: 'SF Mono', 'Consolas', 'Menlo', 'Monaco', monospace; } .csk-msg--ai .csk-msg__bubble .csk-md-ul, .csk-msg--ai .csk-msg__bubble .csk-md-ol { padding-left: 22px; 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: 2px 12px; margin: 8px 0; color: #6B7280; background: rgba(var(--csk-primary-rgb), 0.05); border-radius: 0 6px 6px 0; } .csk-md-link { color: var(--csk-primary); text-decoration: none; border-bottom: 1px solid rgba(var(--csk-primary-rgb), 0.3); transition: border-color 0.15s; } .csk-md-link:hover { border-bottom-color: var(--csk-primary); } .csk-md-hr { border: none; border-top: 1px solid var(--csk-border); margin: 12px 0; } /* ========== P2: 会话管理面板 ========== */ .csk-history-panel { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: #fff; z-index: 10; display: flex; flex-direction: column; overflow: hidden; animation: csk-slide-in 0.24s ease; } .csk-history-panel--hidden { display: none; } @keyframes csk-slide-in { from { transform: translateX(100%); opacity: 0.4; } to { transform: translateX(0); opacity: 1; } } .csk-history-panel__header { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: 1px solid var(--csk-border); background: var(--csk-bg-app); flex-shrink: 0; } .csk-history-panel__title { font-size: 14px; font-weight: 600; color: #1F2937; } .csk-history-panel__back { display: flex; align-items: center; gap: 4px; padding: 5px 12px; border: 1px solid var(--csk-border); border-radius: 8px; background: #fff; color: #374151; font-size: 12px; cursor: pointer; transition: all 0.15s; font-family: inherit; } .csk-history-panel__back:hover { background: #F3F4F6; } .csk-history-panel__list { flex: 1; overflow-y: auto; padding: 10px; } .csk-history-panel__list::-webkit-scrollbar { width: 5px; } .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: 11px 12px; border-radius: 10px; cursor: pointer; transition: background 0.15s; margin-bottom: 5px; border: 1px solid transparent; } .csk-history-item:hover { background: #F3F4F6; } .csk-history-item--active { background: rgba(var(--csk-primary-rgb), 0.08); border-color: rgba(var(--csk-primary-rgb), 0.2); } .csk-history-item--active:hover { background: rgba(var(--csk-primary-rgb), 0.12); } .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: 3px; } .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: 7px; 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: 48px 20px; color: #9CA3AF; font-size: 13px; text-align: center; } .csk-history-panel__empty-icon { font-size: 34px; margin-bottom: 10px; opacity: 0.5; } .csk-history-panel__loading { display: flex; align-items: center; justify-content: center; padding: 24px; 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; } /* ========== 快捷问题芯片 ========== */ .csk-quick-replies { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 18px; max-width: 320px; } .csk-quick-reply { border: 1px solid var(--csk-border); background: #fff; color: #374151; font-size: 12.5px; padding: 7px 13px; border-radius: 999px; cursor: pointer; font-family: inherit; line-height: 1.3; transition: all 0.18s ease; max-width: 100%; } .csk-quick-reply:hover { border-color: var(--csk-primary); color: var(--csk-primary); background: rgba(var(--csk-primary-rgb), 0.06); transform: translateY(-1px); } .csk-quick-reply:active { transform: scale(0.96); } /* ========== AI 消息操作条 ========== */ .csk-msg__actions { display: flex; gap: 2px; margin-top: 5px; padding: 0 2px; opacity: 0; transition: opacity 0.15s ease; } .csk-msg--ai:hover .csk-msg__actions { opacity: 1; } .csk-msg--streaming .csk-msg__actions { display: none; } .csk-action-btn { display: flex; align-items: center; justify-content: center; width: 26px; height: 26px; border: none; background: transparent; color: #9CA3AF; border-radius: 6px; cursor: pointer; transition: all 0.15s ease; } .csk-action-btn:hover { background: #F3F4F6; color: #374151; } .csk-action-btn--done { color: #10B981; } .csk-action-btn:active { transform: scale(0.9); } /* ========== 流式打字光标 ========== */ .csk-caret { display: inline-block; width: 7px; height: 15px; margin-left: 2px; vertical-align: text-bottom; background: var(--csk-primary); border-radius: 1px; animation: csk-caret-blink 1s step-end infinite; } @keyframes csk-caret-blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } } /* ========== 代码块复制按钮 ========== */ .csk-md-code-block { position: relative; } .csk-md-code-copy { position: absolute; top: 8px; right: 8px; display: flex; align-items: center; justify-content: center; width: 26px; height: 26px; border: none; background: rgba(255, 255, 255, 0.12); color: #CBD5E1; border-radius: 6px; cursor: pointer; opacity: 0; transition: all 0.15s ease; } .csk-md-code-block:hover .csk-md-code-copy { opacity: 1; } .csk-md-code-copy:hover { background: rgba(255, 255, 255, 0.22); color: #fff; } .csk-md-code-copy:active { transform: scale(0.9); } /* ========== 新消息提示按钮 ========== */ .csk-newmsg { position: absolute; bottom: 82px; left: 50%; transform: translateX(-50%); z-index: 5; display: inline-flex; align-items: center; gap: 5px; padding: 7px 14px; border: none; background: var(--csk-primary); color: #fff; font-size: 12px; font-family: inherit; border-radius: 999px; cursor: pointer; box-shadow: 0 4px 14px rgba(var(--csk-primary-rgb), 0.38); transition: transform 0.2s ease, box-shadow 0.2s ease; animation: csk-msg-in 0.25s ease; white-space: nowrap; } .csk-newmsg:hover { transform: translateX(-50%) translateY(-1px); box-shadow: 0 6px 18px rgba(var(--csk-primary-rgb), 0.48); } .csk-newmsg:active { transform: translateX(-50%) scale(0.95); } .csk-newmsg--hidden { display: none; } /* ========== 发送按钮停止态 ========== */ .csk-send-btn--stop { background: #EF4444 !important; box-shadow: 0 3px 10px rgba(239, 68, 68, 0.35) !important; } .csk-send-btn--stop:hover:not(:disabled) { box-shadow: 0 5px 14px rgba(239, 68, 68, 0.45) !important; } /* ========== 暗色模式 ========== */ .csk-root.csk-dark { --csk-bg-ai: #1E1E2E; --csk-text-ai: #E2E8F0; --csk-border: #3B3B52; --csk-bg-app: #141421; color: #E2E8F0; } .csk-dark .csk-window { background: #1A1A2E; box-shadow: 0 12px 48px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(0, 0, 0, 0.2); } .csk-dark .csk-header::after { background: linear-gradient(to bottom, rgba(0,0,0,0.12), transparent); } .csk-dark .csk-msg__avatar--ai { background: var(--csk-bg-ai); color: var(--csk-primary); border-color: var(--csk-border); } .csk-dark .csk-input-area { background: #1A1A2E; border-top-color: var(--csk-border); } .csk-dark .csk-input-wrap { background: var(--csk-bg-app); border-color: var(--csk-border); } .csk-dark .csk-input-wrap--focus { background: #1A1A2E; border-color: var(--csk-primary); } .csk-dark .csk-input { color: #E2E8F0; } .csk-dark .csk-loading__avatar { background: var(--csk-bg-ai); border-color: var(--csk-border); } .csk-dark .csk-loading__bubble { background: var(--csk-bg-ai); border-color: var(--csk-border); } .csk-dark .csk-loading__dot { background: #6B7280; } .csk-dark .csk-category-bar { background: #1A1A2E; border-top-color: var(--csk-border); } .csk-dark .csk-category-select { background-color: var(--csk-bg-app); border-color: var(--csk-border); color: #E2E8F0; } .csk-dark .csk-sources { background: #1E1E2E; border-color: var(--csk-border); } .csk-dark .csk-sources__header { background: #25253A; } .csk-dark .csk-sources__header:hover { background: #2D2D48; } .csk-dark .csk-source-item:hover { background: var(--csk-bg-ai); } .csk-dark .csk-source-item__name { color: #E2E8F0; } .csk-dark .csk-source-item__meta { color: #6B7280; } .csk-dark .csk-history-panel { background: #1A1A2E; } .csk-dark .csk-history-panel__header { background: var(--csk-bg-app); border-bottom-color: var(--csk-border); } .csk-dark .csk-history-panel__back { background: var(--csk-bg-app); border-color: var(--csk-border); color: #E2E8F0; } .csk-dark .csk-history-panel__back:hover { background: #2D2D48; } .csk-dark .csk-history-item:hover { background: #25253A; } .csk-dark .csk-history-item__id { color: #E2E8F0; } .csk-dark .csk-history-item__meta { color: #6B7280; } .csk-dark .csk-history-item--active { background: rgba(var(--csk-primary-rgb), 0.15); } .csk-dark .csk-history-item--active:hover { background: rgba(var(--csk-primary-rgb), 0.22); } .csk-dark .csk-action-btn:hover { background: #2D2D48; color: #E2E8F0; } .csk-dark .csk-clear-btn { background: var(--csk-bg-app); border-color: var(--csk-border); color: #9CA3AF; } .csk-dark .csk-clear-btn:hover { background: #3B1A1A; border-color: #7F1D1D; color: #FCA5A5; } .csk-dark .csk-md-code-block { background: #0F0F1A; } .csk-dark .csk-md-blockquote { background: rgba(var(--csk-primary-rgb), 0.08); } .csk-dark .csk-md-link { color: var(--csk-primary-light); } .csk-dark .csk-msg--ai .csk-msg__bubble { background: var(--csk-bg-ai); color: var(--csk-text-ai); border-color: var(--csk-border); } .csk-dark .csk-welcome__title { color: #E2E8F0; } .csk-dark .csk-welcome__desc { color: #6B7280; } .csk-dark .csk-quick-reply { background: var(--csk-bg-app); border-color: var(--csk-border); color: #CBD5E1; } .csk-dark .csk-quick-reply:hover { background: rgba(var(--csk-primary-rgb), 0.12); border-color: var(--csk-primary); color: var(--csk-primary-light); } .csk-dark .csk-teaser { background: #2D2D48; color: #E2E8F0; box-shadow: 0 4px 20px rgba(0,0,0,0.35); } .csk-dark .csk-teaser::after { border-top-color: #2D2D48; } .csk-dark .csk-teaser__close { color: #6B7280; } .csk-dark .csk-teaser__close:hover { color: #9CA3AF; } .csk-dark .csk-feedback-btn { background: rgba(var(--csk-primary-rgb), 0.1); color: #9CA3AF; } .csk-dark .csk-feedback-btn:hover { background: rgba(var(--csk-primary-rgb), 0.2); color: var(--csk-primary-light); } .csk-dark .csk-feedback-btn--active { background: rgba(var(--csk-primary-rgb), 0.28); color: var(--csk-primary-light); } .csk-dark .csk-history-panel__search-wrap { background: var(--csk-bg-app); border-color: var(--csk-border); } .csk-dark .csk-history-panel__search-wrap:focus-within { border-color: var(--csk-primary); } .csk-dark .csk-history-panel__search { color: #E2E8F0; } .csk-dark .csk-launcher__badge { box-shadow: 0 0 0 2px #1A1A2E; } .csk-dark .csk-welcome__avatar { box-shadow: 0 6px 18px rgba(var(--csk-primary-rgb), 0.25); } .csk-dark .csk-header__avatar { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.1); } /* ========== Launcher 未读徽章 ========== */ .csk-launcher__badge { position: absolute; top: -2px; right: -2px; width: 16px; height: 16px; border-radius: 50%; background: #EF4444; border: 2px solid #fff; box-shadow: 0 0 0 2px #fff; animation: csk-badge-in 0.28s cubic-bezier(0.34, 1.56, 0.64, 1); pointer-events: none; } .csk-launcher__badge--hidden { display: none; } @keyframes csk-badge-in { from { transform: scale(0); } to { transform: scale(1); } } /* ========== Launcher 提示气泡 ========== */ .csk-teaser { position: fixed; bottom: 90px; z-index: 9997; max-width: 240px; padding: 10px 14px; background: #fff; color: #1F2937; font-size: 13px; line-height: 1.5; border-radius: 12px; box-shadow: 0 4px 20px rgba(15, 23, 42, 0.15); animation: csk-teaser-in 0.35s cubic-bezier(0.22, 1, 0.36, 1); cursor: pointer; user-select: none; } .csk-teaser--right { right: 24px; } .csk-teaser--left { left: 24px; } .csk-teaser::after { content: ''; position: absolute; bottom: -6px; right: 28px; width: 12px; height: 12px; background: inherit; border-radius: 2px; transform: rotate(45deg); box-shadow: 2px 2px 4px rgba(15, 23, 42, 0.08); } .csk-teaser--left::after { right: auto; left: 28px; } .csk-teaser__close { position: absolute; top: 4px; right: 6px; background: none; border: none; font-size: 16px; color: #9CA3AF; cursor: pointer; padding: 2px 4px; line-height: 1; border-radius: 4px; } .csk-teaser__close:hover { color: #374151; } .csk-teaser--hidden { display: none; } @keyframes csk-teaser-in { from { opacity: 0; transform: translateY(8px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } } /* ========== 消息反馈按钮(👍 / 👎) ========== */ .csk-feedback-btn { display: flex; align-items: center; justify-content: center; width: 26px; height: 26px; border: none; background: rgba(var(--csk-primary-rgb), 0.08); color: #9CA3AF; border-radius: 6px; cursor: pointer; transition: all 0.15s ease; padding: 0; } .csk-feedback-btn:hover { background: rgba(var(--csk-primary-rgb), 0.18); color: var(--csk-primary); } .csk-feedback-btn--active { background: rgba(var(--csk-primary-rgb), 0.28); color: var(--csk-primary); } .csk-feedback-btn--active.down { background: #FEF2F2; color: #EF4444; } .csk-feedback-btn:active { transform: scale(0.9); } .csk-msg--streaming .csk-feedback-btn { display: none; } /* ========== 历史会话搜索框 ========== */ .csk-history-panel__search-wrap { padding: 8px 10px; border-bottom: 1px solid var(--csk-border); flex-shrink: 0; } .csk-history-panel__search-wrap:focus-within { border-bottom-color: var(--csk-primary); } .csk-history-panel__search { width: 100%; padding: 7px 10px; border: 1px solid var(--csk-border); border-radius: 8px; font-size: 12.5px; font-family: inherit; color: #1F2937; background: var(--csk-bg-app); outline: none; transition: border-color 0.2s; } .csk-history-panel__search:focus { border-color: var(--csk-primary); } .csk-history-panel__search::placeholder { color: #9CA3AF; } /* ========== a11y 焦点指示 ========== */ .csk-root :focus-visible { outline: 2px solid var(--csk-primary); outline-offset: 2px; } .csk-root button:focus-visible { outline: 2px solid var(--csk-primary); outline-offset: 2px; border-radius: inherit; } /* 弹窗打开时隐藏 launcher 上的焦点环 */ .csk-launcher:focus-visible { outline: none; border-color: rgba(255, 255, 255, 0.75); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06), 0 4px 16px rgba(0, 0, 0, 0.10), 0 6px 24px rgba(var(--csk-primary-rgb), 0.28), 0 0 48px rgba(var(--csk-primary-rgb), 0.12), 0 0 0 4px rgba(var(--csk-primary-rgb), 0.3); } /* ========== a11y aria-live 播报区(视觉隐藏) ========== */ .csk-sr-only { position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; margin: -1px !important; overflow: hidden !important; clip: rect(0,0,0,0) !important; white-space: nowrap !important; border: 0 !important; } /* ========== 会话分组标题 ========== */ .csk-history-group { margin-bottom: 4px; } .csk-history-group__label { font-size: 11px; font-weight: 600; color: #9CA3AF; text-transform: uppercase; letter-spacing: 0.5px; padding: 8px 12px 4px; user-select: none; } .csk-dark .csk-history-group__label { color: #6B7280; } /* ========== 移动端适配 ========== */ @media (max-width: 480px) { .csk-window { width: 100vw !important; height: 100vh !important; max-height: 100vh; bottom: 0 !important; right: 0 !important; left: 0 !important; border-radius: 0; } .csk-window--hidden { transform: translateY(100%); } .csk-launcher { bottom: 20px; } .csk-launcher--right { right: 20px; } .csk-launcher--left { left: 20px; } } `; } /** * 注入样式到 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; } 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(); } // ==================== 图标常量 ==================== /** 机器人头像图标(用于头部、欢迎态、AI 气泡、Loading) */ const BOT_ICON = ``; /** 用户头像图标 */ const USER_ICON = ``; // ==================== 悬浮按钮 ==================== /** 创建悬浮按钮 */ function createLauncher(config, onClick) { const launcher = document.createElement('div'); launcher.id = 'csk-launcher'; // 有玻璃主题时追加 --glass 类名,启用磨砂玻璃质感样式 const glassClass = config.launcherTheme ? ' csk-launcher--glass' : ''; launcher.className = `csk-launcher csk-launcher--${config.position === 'left-bottom' ? 'left' : 'right'}${glassClass}`; launcher.setAttribute('title', config.title); launcher.setAttribute('aria-label', config.title); launcher.setAttribute('role', 'button'); launcher.setAttribute('tabindex', '0'); // 图标内容 launcher.innerHTML = config.launcherIcon; // 未读徽章(初始隐藏,有未读消息时由 index.ts 显示) const badge = document.createElement('div'); badge.className = 'csk-launcher__badge csk-launcher__badge--hidden'; launcher.appendChild(badge); // 点击事件(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${config.theme === 'dark' ? ' csk-dark' : ''}`; windowEl.setAttribute('role', 'dialog'); windowEl.setAttribute('aria-label', config.title); windowEl.setAttribute('aria-modal', 'false'); // === 头部 === const header = document.createElement('div'); header.className = 'csk-header'; const headerLeft = document.createElement('div'); headerLeft.className = 'csk-header__left'; const headerAvatar = document.createElement('div'); headerAvatar.className = 'csk-header__avatar'; headerAvatar.innerHTML = BOT_ICON; const headerInfo = document.createElement('div'); headerInfo.className = 'csk-header__info'; const titleEl = document.createElement('span'); titleEl.className = 'csk-header__title'; titleEl.textContent = config.title; const statusEl = document.createElement('span'); statusEl.className = 'csk-header__status'; statusEl.innerHTML = `${t('status_online')}`; headerInfo.appendChild(titleEl); headerInfo.appendChild(statusEl); headerLeft.appendChild(headerAvatar); headerLeft.appendChild(headerInfo); 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(headerLeft); header.appendChild(actions); // === 消息区 === const messagesContainer = document.createElement('div'); messagesContainer.id = 'csk-messages'; messagesContainer.className = 'csk-messages'; // === 欢迎空状态 === const welcomeEl = document.createElement('div'); welcomeEl.className = 'csk-welcome'; welcomeEl.innerHTML = `
${BOT_ICON}
${t('welcome_title')}
${t('welcome_desc')}
`; // 快捷问题芯片(点击即自动发送) if (config.quickReplies.length > 0) { const chips = document.createElement('div'); chips.className = 'csk-quick-replies'; for (const reply of config.quickReplies) { const chip = document.createElement('button'); chip.type = 'button'; chip.className = 'csk-quick-reply'; chip.textContent = reply; chip.addEventListener('click', () => { windowEl.dispatchEvent(new CustomEvent('csk:quickReply', { detail: { text: reply } })); }); chips.appendChild(chip); } welcomeEl.appendChild(chips); } messagesContainer.appendChild(welcomeEl); // === 新消息提示按钮(用户上滑时新消息到达显示) === // 挂到 windowEl 而非 messagesContainer,绝对定位悬浮于输入区上方,避免随内容滚动 const newMsgBtn = document.createElement('button'); newMsgBtn.type = 'button'; newMsgBtn.className = 'csk-newmsg csk-newmsg--hidden'; newMsgBtn.innerHTML = ` ${t('new_message')} `; windowEl.appendChild(newMsgBtn); // === 会话管理面板(P2,默认隐藏) === const historyPanel = document.createElement('div'); historyPanel.className = 'csk-history-panel csk-history-panel--hidden'; historyPanel.innerHTML = `
${t('history_title')}
`; messagesContainer.appendChild(historyPanel); // 历史面板返回按钮 const backBtn = historyPanel.querySelector('#csk-history-back'); if (backBtn) { backBtn.addEventListener('click', () => { historyPanel.classList.add('csk-history-panel--hidden'); }); } // 历史按钮点击 historyBtn.addEventListener('click', (e) => { e.stopPropagation(); const isHidden = historyPanel.classList.contains('csk-history-panel--hidden'); historyPanel.classList.toggle('csk-history-panel--hidden'); if (isHidden) { // 触发自定义事件,通知加载会话列表 windowEl.dispatchEvent(new CustomEvent('csk:loadHistory')); } }); // === 知识库分类下拉框(P1) === let categorySelect = null; if (config.showCategorySwitch) { const categoryBar = document.createElement('div'); categoryBar.className = 'csk-category-bar'; const categoryLabel = document.createElement('span'); categoryLabel.className = 'csk-category-bar__label'; categoryLabel.textContent = '📚'; categorySelect = document.createElement('select'); categorySelect.id = 'csk-category-select'; categorySelect.className = 'csk-category-select'; categorySelect.innerHTML = ``; // onChange 触发自定义事件 categorySelect.addEventListener('change', () => { const selectedId = categorySelect.value; windowEl.dispatchEvent(new CustomEvent('csk:categoryChange', { detail: { categoryId: selectedId ? Number(selectedId) : undefined } })); }); categoryBar.appendChild(categoryLabel); categoryBar.appendChild(categorySelect); // 插入到 messages 和 inputArea 之间 windowEl.appendChild(header); windowEl.appendChild(messagesContainer); windowEl.appendChild(categoryBar); } else { windowEl.appendChild(header); windowEl.appendChild(messagesContainer); } // === 输入区 === const inputArea = document.createElement('div'); inputArea.className = 'csk-input-area'; const inputWrap = document.createElement('div'); inputWrap.className = 'csk-input-wrap'; const inputEl = document.createElement('textarea'); inputEl.id = 'csk-input'; inputEl.className = 'csk-input'; inputEl.setAttribute('placeholder', t('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', t('send')); sendBtn.setAttribute('disabled', 'true'); sendBtn.innerHTML = ``; inputWrap.appendChild(inputEl); inputWrap.appendChild(sendBtn); inputArea.appendChild(inputWrap); windowEl.appendChild(inputArea); // 清空按钮(可选) let clearBtn = null; if (config.showClear) { clearBtn = document.createElement('button'); clearBtn.className = 'csk-clear-btn'; clearBtn.textContent = t('clear'); clearBtn.style.display = 'none'; // 初始隐藏,有消息后才显示 // 插入到 categoryBar/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 = `
${BOT_ICON}
`; messagesContainer.appendChild(el); loadingEl = el; return el; } function hideLoading() { if (loadingEl && loadingEl.parentNode) { loadingEl.parentNode.removeChild(loadingEl); loadingEl = null; } } // === a11y: aria-live 播报区域(视觉隐藏,供屏幕阅读器监听新消息) === const ariaLiveEl = document.createElement('div'); ariaLiveEl.className = 'csk-sr-only'; ariaLiveEl.setAttribute('aria-live', 'polite'); ariaLiveEl.setAttribute('aria-atomic', 'true'); windowEl.appendChild(ariaLiveEl); // === 历史搜索输入框引用 === const searchInput = historyPanel.querySelector('#csk-history-search'); // === Launcher 提示气泡(首访引导,初始隐藏,由 index.ts 定时控制) === const teaserEl = document.createElement('div'); teaserEl.className = `csk-teaser csk-teaser--${config.position === 'left-bottom' ? 'left' : 'right'} csk-teaser--hidden`; teaserEl.setAttribute('role', 'status'); teaserEl.innerHTML = ` ${config.teaserText || t('teaser_text')} `; return { window: windowEl, messagesContainer, inputEl, sendBtn, clearBtn, categorySelect, historyPanel, welcomeEl, newMsgBtn, teaserEl, searchInput, ariaLiveEl, showLoading, hideLoading, }; } // ==================== 拖拽支持 ==================== /** 启用弹窗拖拽,onDragEnd 回调返回最终位置用于持久化 */ function enableDrag(headerEl, windowEl, onDragEnd) { let dragging = false; let startX = 0; let startY = 0; let offsetX = 0; let offsetY = 0; const onMouseDown = (e) => { // 忽略头部按钮点击触发的拖拽 const target = e.target; if (target.closest('button')) return; 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); // 拖拽结束回调:返回当前位置供持久化 if (onDragEnd) { const rect = windowEl.getBoundingClientRect(); onDragEnd({ x: rect.left, y: rect.top }); } }; 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 avatar = document.createElement('div'); avatar.className = 'csk-msg__avatar csk-msg__avatar--user'; avatar.innerHTML = USER_ICON; const content = document.createElement('div'); content.className = 'csk-msg__content'; 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); content.appendChild(bubble); content.appendChild(time); wrapper.appendChild(avatar); wrapper.appendChild(content); container.appendChild(wrapper); return wrapper; } /** 渲染 AI 消息气泡(支持 Markdown) */ function renderAIBubble(container, text, timestamp, renderMd, msgId) { const wrapper = document.createElement('div'); wrapper.className = 'csk-msg csk-msg--ai'; if (msgId) wrapper.dataset.cskMsgId = msgId; const avatar = document.createElement('div'); avatar.className = 'csk-msg__avatar csk-msg__avatar--ai'; avatar.innerHTML = BOT_ICON; const content = document.createElement('div'); content.className = 'csk-msg__content'; const bubble = document.createElement('div'); bubble.className = 'csk-msg__bubble'; // 支持 Markdown 渲染,传入渲染函数则使用,否则纯文本 if (renderMd) { bubble.innerHTML = renderMd(text); } else { bubble.textContent = text; } const time = document.createElement('div'); time.className = 'csk-msg__time'; time.textContent = formatTime(timestamp); content.appendChild(bubble); // AI 操作条(复制 / 重试),插入到气泡与时间戳之间 content.appendChild(buildAIActions(bubble, wrapper)); content.appendChild(time); wrapper.appendChild(avatar); wrapper.appendChild(content); container.appendChild(wrapper); // 代码块复制按钮 enhanceCodeBlocks(bubble); return wrapper; } /** 创建空的 AI 气泡(流式追加用) */ function createEmptyAIBubble(container, timestamp, msgId) { const wrapper = document.createElement('div'); wrapper.className = 'csk-msg csk-msg--ai csk-msg--streaming'; if (msgId) wrapper.dataset.cskMsgId = msgId; const avatar = document.createElement('div'); avatar.className = 'csk-msg__avatar csk-msg__avatar--ai'; avatar.innerHTML = BOT_ICON; const content = document.createElement('div'); content.className = 'csk-msg__content'; 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); content.appendChild(bubble); content.appendChild(buildAIActions(bubble, wrapper)); content.appendChild(time); wrapper.appendChild(avatar); wrapper.appendChild(content); container.appendChild(wrapper); return { wrapper, bubble }; } // ==================== AI 消息操作条 + 代码块复制 ==================== /** 复制图标 */ const COPY_ICON = ``; /** 重试图标 */ const RETRY_ICON = ``; /** 构建 AI 消息操作条(复制 / 重试 / 反馈) */ function buildAIActions(bubble, wrapper) { const bar = document.createElement('div'); bar.className = 'csk-msg__actions'; // 复制按钮 const copyBtn = document.createElement('button'); copyBtn.type = 'button'; copyBtn.className = 'csk-action-btn'; copyBtn.setAttribute('title', t('copy')); copyBtn.setAttribute('aria-label', t('copy')); copyBtn.innerHTML = COPY_ICON; copyBtn.addEventListener('click', async (e) => { e.stopPropagation(); const text = bubble.textContent || ''; await copyToClipboard(text, copyBtn); }); // 重试按钮 const retryBtn = document.createElement('button'); retryBtn.type = 'button'; retryBtn.className = 'csk-action-btn'; retryBtn.setAttribute('title', t('retry')); retryBtn.setAttribute('aria-label', t('retry')); retryBtn.innerHTML = RETRY_ICON; retryBtn.addEventListener('click', (e) => { e.stopPropagation(); const root = wrapper.closest('.csk-window'); if (root) { root.dispatchEvent(new CustomEvent('csk:retry', { detail: { msgId: wrapper.dataset.cskMsgId || '' } })); } }); // 反馈按钮(👍 / 👎) const THUMB_UP = ``; const THUMB_DOWN = ``; const upBtn = document.createElement('button'); upBtn.type = 'button'; upBtn.className = 'csk-feedback-btn'; upBtn.setAttribute('title', t('feedback_up')); upBtn.setAttribute('aria-label', t('feedback_up')); upBtn.innerHTML = THUMB_UP; upBtn.addEventListener('click', (e) => { e.stopPropagation(); const root = wrapper.closest('.csk-window'); if (root) { root.dispatchEvent(new CustomEvent('csk:feedback', { detail: { msgId: wrapper.dataset.cskMsgId || '', value: 'up' } })); } }); const downBtn = document.createElement('button'); downBtn.type = 'button'; downBtn.className = 'csk-feedback-btn'; downBtn.setAttribute('title', t('feedback_down')); downBtn.setAttribute('aria-label', t('feedback_down')); downBtn.innerHTML = THUMB_DOWN; downBtn.addEventListener('click', (e) => { e.stopPropagation(); const root = wrapper.closest('.csk-window'); if (root) { root.dispatchEvent(new CustomEvent('csk:feedback', { detail: { msgId: wrapper.dataset.cskMsgId || '', value: 'down' } })); } }); bar.appendChild(copyBtn); bar.appendChild(retryBtn); bar.appendChild(upBtn); bar.appendChild(downBtn); return bar; } /** * 更新反馈按钮的视觉状态(由 chat.ts 调用) * @param wrapper 消息 wrapper 元素 * @param value 当前反馈值:'up' | 'down' | undefined */ function updateFeedbackUI(wrapper, value) { const btns = wrapper.querySelectorAll('.csk-feedback-btn'); if (btns.length < 2) return; btns.forEach((btn) => btn.classList.remove('csk-feedback-btn--active', 'down')); if (value === 'up' && btns[0]) btns[0].classList.add('csk-feedback-btn--active'); if (value === 'down' && btns[1]) btns[1].classList.add('csk-feedback-btn--active', 'down'); } /** 复制文本到剪贴板,带"已复制"反馈 */ async function copyToClipboard(text, btn) { const original = btn.innerHTML; try { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); } else { // 降级方案:execCommand const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } btn.innerHTML = ``; btn.classList.add('csk-action-btn--done'); setTimeout(() => { btn.innerHTML = original; btn.classList.remove('csk-action-btn--done'); }, 1500); } catch (_a) { // 复制失败静默处理 } } /** 为气泡内所有代码块添加复制按钮 */ function enhanceCodeBlocks(bubble) { const blocks = bubble.querySelectorAll('.csk-md-code-block'); blocks.forEach((pre) => { if (pre.querySelector('.csk-md-code-copy')) return; const codeEl = pre.querySelector('code'); const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'csk-md-code-copy'; btn.setAttribute('title', t('copy')); btn.setAttribute('aria-label', t('copy')); btn.innerHTML = COPY_ICON; btn.addEventListener('click', async (e) => { e.stopPropagation(); await copyToClipboard((codeEl === null || codeEl === void 0 ? void 0 : codeEl.textContent) || '', btn); }); pre.appendChild(btn); }); } /** 流式打字光标:追加到气泡末尾(每次 chunk 后调用) */ function appendStreamingCaret(bubble) { if (bubble.querySelector('.csk-caret')) return; const caret = document.createElement('span'); caret.className = 'csk-caret'; caret.setAttribute('aria-hidden', 'true'); bubble.appendChild(caret); } /** 标记 AI 气泡流式结束:移除流式态、补代码块复制按钮 */ function finalizeAIBubble(wrapper, bubble) { wrapper.classList.remove('csk-msg--streaming'); const caret = bubble.querySelector('.csk-caret'); if (caret) caret.remove(); enhanceCodeBlocks(bubble); } // ==================== P1: RAG 引用来源渲染 ==================== /** 渲染 RAG 引用来源卡片 */ function renderSources(wrapper, sources) { // 移除已有的来源卡片 const existing = wrapper.querySelector('.csk-sources'); if (existing) existing.remove(); if (!sources || sources.length === 0) return; const sourcesEl = document.createElement('div'); sourcesEl.className = 'csk-sources csk-sources--collapsed'; // 头部 const header = document.createElement('div'); header.className = 'csk-sources__header'; const titleSpan = document.createElement('span'); titleSpan.className = 'csk-sources__title'; titleSpan.textContent = `📚 ${t('source_count', { n: sources.length })}`; const arrow = document.createElement('span'); arrow.className = 'csk-sources__arrow'; arrow.textContent = '▼'; header.appendChild(titleSpan); header.appendChild(arrow); // 点击折叠/展开 header.addEventListener('click', () => { sourcesEl.classList.toggle('csk-sources--collapsed'); }); // 内容 const body = document.createElement('div'); body.className = 'csk-sources__body'; for (const src of sources) { const item = document.createElement('div'); item.className = 'csk-source-item'; const name = document.createElement('div'); name.className = 'csk-source-item__name'; name.textContent = src.title || src.sourceName || '未知文档'; if (src.snippet) { const snippet = document.createElement('div'); snippet.className = 'csk-source-item__snippet'; snippet.textContent = src.snippet; item.appendChild(snippet); } const meta = document.createElement('div'); meta.className = 'csk-source-item__meta'; const metaParts = []; if (src.sourceName) metaParts.push(src.sourceName); if (src.chunkIndex !== undefined) metaParts.push(`分块 #${src.chunkIndex}`); if (src.score !== undefined) metaParts.push(`相关度 ${(src.score * 100).toFixed(0)}%`); meta.textContent = metaParts.join(' · '); item.appendChild(name); item.appendChild(meta); body.appendChild(item); } sourcesEl.appendChild(header); sourcesEl.appendChild(body); // 插入到气泡和时间戳之间(时间戳在 content 容器内,故用其父节点插入) const timeEl = wrapper.querySelector('.csk-msg__time'); if (timeEl && timeEl.parentNode) { timeEl.parentNode.insertBefore(sourcesEl, timeEl); } else { wrapper.appendChild(sourcesEl); } } /** 按日期分组会话列表:今天 / 昨天 / 本周 / 更早 */ function groupHistoryByDate(items) { const now = new Date(); const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); const yesterdayStart = todayStart - 86400000; const weekStart = todayStart - (now.getDay() || 7) * 86400000 + 86400000; // 本周一 const groups = { today: [], yesterday: [], week: [], earlier: [], }; for (const item of items) { // 尝试解析时间:优先 lastMessageTime,其次 createdAt const timeStr = item.lastMessageTime || item.createdAt; const ts = timeStr ? new Date(timeStr).getTime() : 0; if (ts >= todayStart) { groups.today.push(item); } else if (ts >= yesterdayStart) { groups.yesterday.push(item); } else if (ts >= weekStart) { groups.week.push(item); } else { groups.earlier.push(item); } } const result = []; if (groups.today.length) result.push({ label: t('history_group_today'), items: groups.today }); if (groups.yesterday.length) result.push({ label: t('history_group_yesterday'), items: groups.yesterday }); if (groups.week.length) result.push({ label: t('history_group_week'), items: groups.week }); if (groups.earlier.length) result.push({ label: t('history_group_earlier'), items: groups.earlier }); return result; } /** 渲染会话列表(带日期分组) */ function renderHistoryList(listEl, items, onSelect, onExport, onDelete, activeChatId, emptyText) { listEl.innerHTML = ''; if (items.length === 0) { const empty = document.createElement('div'); empty.className = 'csk-history-panel__empty'; empty.innerHTML = `
💬
${t('history_empty')}
`; listEl.appendChild(empty); return; } // 按日期分组渲染 const groups = groupHistoryByDate(items); for (const group of groups) { // 分组标题 if (groups.length > 1) { const groupEl = document.createElement('div'); groupEl.className = 'csk-history-group'; const labelEl = document.createElement('div'); labelEl.className = 'csk-history-group__label'; labelEl.textContent = group.label; groupEl.appendChild(labelEl); listEl.appendChild(groupEl); } for (const item of group.items) { const el = document.createElement('div'); el.className = 'csk-history-item'; // 高亮当前活跃会话 const convId = item.chatId || item.id; if (activeChatId && convId === activeChatId) { el.classList.add('csk-history-item--active'); } const info = document.createElement('div'); info.className = 'csk-history-item__info'; const idEl = document.createElement('div'); idEl.className = 'csk-history-item__id'; // 显示最后一条消息预览,没有则显示 chatId if (item.lastMessagePreview) { idEl.textContent = item.lastMessagePreview.length > 60 ? item.lastMessagePreview.substring(0, 60) + '...' : item.lastMessagePreview; } else { idEl.textContent = convId; } const metaEl = document.createElement('div'); metaEl.className = 'csk-history-item__meta'; const metaParts = []; if (item.messageCount !== undefined) metaParts.push(`${item.messageCount} 条消息`); if (item.lastMessageTime) metaParts.push(item.lastMessageTime); else if (item.createdAt) metaParts.push(item.createdAt); metaEl.textContent = metaParts.join(' · '); info.appendChild(idEl); info.appendChild(metaEl); const actionsEl = document.createElement('div'); actionsEl.className = 'csk-history-item__actions'; // 导出按钮 const exportBtn = document.createElement('button'); exportBtn.className = 'csk-history-action csk-history-action--export'; exportBtn.setAttribute('title', t('history_export')); exportBtn.innerHTML = ``; exportBtn.addEventListener('click', (e) => { e.stopPropagation(); onExport(item.id); }); // 删除按钮 const deleteBtn = document.createElement('button'); deleteBtn.className = 'csk-history-action csk-history-action--delete'; deleteBtn.setAttribute('title', t('history_delete')); deleteBtn.innerHTML = ``; deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); onDelete(item.id); }); actionsEl.appendChild(exportBtn); actionsEl.appendChild(deleteBtn); el.appendChild(info); el.appendChild(actionsEl); // 点击整行 → 切换到该会话 el.addEventListener('click', () => { onSelect(convId); }); listEl.appendChild(el); } } // 关闭 group 循环 } /** 滚动消息区到底部 */ 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); } } /** * 轻量级 Markdown 渲染器 - 无外部依赖,XSS 安全 * * 支持:代码块、行内代码、标题、粗体、斜体、列表、链接、引用、段落 * 策略:先转义 HTML,再转 Markdown 为安全 HTML 标签 */ /** 代码块占位符前缀 */ const CODE_BLOCK_PREFIX = '\x00CODEBLOCK_'; /** 行内代码占位符前缀 */ const INLINE_CODE_PREFIX = '\x00INLINECODE_'; /** * 渲染 Markdown 文本为安全 HTML * @param text Markdown 源文本 * @returns 安全 HTML 字符串 */ function renderMarkdown(text) { if (!text || typeof text !== 'string') return ''; // 1. 提取代码块(防止内部 Markdown 被处理) const codeBlocks = []; let processed = text; // 提取围栏代码块 ``` processed = processed.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => { const idx = codeBlocks.length; const escapedCode = escapeHtml(code.trimEnd()); const langClass = lang ? ` class="language-${escapeHtml(lang)}"` : ''; codeBlocks.push(`
${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('' : ''); 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 inputWrap = null; let sendBtn$1 = null; let clearBtn$1 = null; let categorySelect$1 = null; let historyPanel$1 = null; let welcomeEl = null; let newMsgBtn = null; let searchInput = null; let ariaLiveEl = null; let showLoadingFn$1 = null; let hideLoadingFn$1 = null; let isSending = false; /** 当前流式请求的中断控制器(用于"停止生成") */ let abortController = null; /** 用户是否停留在底部附近(用于智能滚动 + 新消息提示) */ let isNearBottom = true; /** 缓存的历史会话列表(供搜索过滤用) */ let historyItems = []; /** 当前历史搜索关键字 */ let historySearchText = ''; /** 当前选中的知识库分类 ID */ let currentCategoryId; /** 当前是否使用 RAG 对话 */ let useRag = false; /** * 初始化对话模块 */ function initChat(cfg, dom) { config$1 = cfg; messagesContainer$1 = dom.messagesContainer; inputEl$1 = dom.inputEl; inputWrap = dom.inputEl.parentElement; sendBtn$1 = dom.sendBtn; clearBtn$1 = dom.clearBtn; categorySelect$1 = dom.categorySelect; historyPanel$1 = dom.historyPanel; welcomeEl = dom.welcomeEl; newMsgBtn = dom.newMsgBtn; searchInput = dom.searchInput; ariaLiveEl = dom.ariaLiveEl; showLoadingFn$1 = dom.showLoading; hideLoadingFn$1 = dom.hideLoading; // 初始化知识库分类 currentCategoryId = cfg.categoryId; useRag = cfg.enableRag; // 绑定发送事件 bindSendEvents(); // 绑定滚动监听(智能滚动 + 新消息提示) bindScrollEvents(); // 绑定历史搜索过滤 bindHistorySearch(); // 加载知识库分类下拉框 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', () => { if (isSending && abortController) { abortController.abort(); return; } handleSend(); }); inputEl$1.addEventListener('keydown', (e) => { // IME 组合输入期间(中文/日文输入法选词回车)不触发发送 if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); handleSend(); } }); inputEl$1.addEventListener('input', () => { updateSendBtnState(); autoResizeInput(); }); // 输入框聚焦/失焦高亮容器 inputEl$1.addEventListener('focus', () => { if (inputWrap) inputWrap.classList.add('csk-input-wrap--focus'); }); inputEl$1.addEventListener('blur', () => { if (inputWrap) inputWrap.classList.remove('csk-input-wrap--focus'); }); if (clearBtn$1) { clearBtn$1.addEventListener('click', () => handleClear()); } } /** 绑定滚动监听:判断是否在底部,控制新消息提示按钮 */ function bindScrollEvents() { if (!messagesContainer$1) return; messagesContainer$1.addEventListener('scroll', () => { if (!messagesContainer$1) return; const { scrollTop, scrollHeight, clientHeight } = messagesContainer$1; // 距底部 80px 内视为"在底部" isNearBottom = scrollHeight - scrollTop - clientHeight < 80; if (isNearBottom) hideNewMsgBtn(); }); // 新消息提示按钮点击 → 回到底部 if (newMsgBtn) { newMsgBtn.addEventListener('click', () => { if (messagesContainer$1) scrollToBottom(messagesContainer$1); isNearBottom = true; hideNewMsgBtn(); }); } } /** 显示新消息提示按钮 */ function showNewMsgBtn() { if (newMsgBtn) newMsgBtn.classList.remove('csk-newmsg--hidden'); } /** 隐藏新消息提示按钮 */ function hideNewMsgBtn() { if (newMsgBtn) newMsgBtn.classList.add('csk-newmsg--hidden'); } /** 智能滚动到底部:用户在底部时自动滚,上滑时显示新消息提示 */ function smartScrollToBottom() { if (!messagesContainer$1) return; if (isNearBottom) { scrollToBottom(messagesContainer$1); } else { showNewMsgBtn(); } } /** 切换发送按钮模式:发送 / 停止生成 */ function setSendButtonMode(mode) { if (!sendBtn$1) return; if (mode === 'stop') { sendBtn$1.classList.add('csk-send-btn--stop'); sendBtn$1.removeAttribute('disabled'); sendBtn$1.setAttribute('title', t('stop')); sendBtn$1.setAttribute('aria-label', t('stop')); sendBtn$1.innerHTML = ``; } else { sendBtn$1.classList.remove('csk-send-btn--stop'); sendBtn$1.setAttribute('title', t('send')); sendBtn$1.setAttribute('aria-label', t('send')); sendBtn$1.innerHTML = ``; updateSendBtnState(); } } // ==================== 历史会话搜索过滤 ==================== /** 绑定历史搜索输入事件 */ function bindHistorySearch() { if (!searchInput) return; searchInput.addEventListener('input', () => { historySearchText = searchInput.value.trim().toLowerCase(); renderFilteredHistory(); }); } /** 按搜索关键字过滤并重新渲染历史列表 */ function renderFilteredHistory() { const listEl = historyPanel$1 === null || historyPanel$1 === void 0 ? void 0 : historyPanel$1.querySelector('#csk-history-list'); if (!listEl || !config$1) return; const filtered = historySearchText ? historyItems.filter(item => { const preview = (item.lastMessagePreview || item.chatId || item.id || '').toLowerCase(); return preview.includes(historySearchText); }) : historyItems; renderHistoryList(listEl, filtered, (conversationId) => { switchToConversation(conversationId); }, (id) => { window.open(getConversationExportUrl(id), '_blank'); }, async (id) => { if (!confirm(t('history_delete_confirm'))) return; const ok = await deleteConversation(id); if (ok) { if (id === getChatId()) { messages = []; if (messagesContainer$1) { messagesContainer$1.querySelectorAll('.csk-msg, .csk-loading').forEach(el => el.remove()); } if (clearBtn$1) clearBtn$1.style.display = 'none'; updateEmptyState(); } // 从缓存中移除并重新渲染 historyItems = historyItems.filter(it => (it.chatId || it.id) !== id); renderFilteredHistory(); } }, getChatId()); } // ==================== a11y 消息播报 ==================== /** 通过 aria-live 区域播报新消息(供屏幕阅读器使用) */ function announceMessage(text) { if (!ariaLiveEl) return; // 截取前 120 字符,避免过长播报 const snippet = text.length > 120 ? text.substring(0, 120) + '...' : text; ariaLiveEl.textContent = t('new_msg_announce') + ':' + snippet; } // ==================== 消息反馈(👍 / 👎) ==================== /** * 处理消息反馈:切换 AI 消息的点赞/点踩状态 * 前端状态持久化(存入 messages 数组 + localStorage)+ 调用后端 API 记录反馈 */ function handleFeedback(msgId, value) { if (!messagesContainer$1) return; const msg = messages.find(m => m.id === msgId && m.role === 'ai'); if (!msg) return; // 切换逻辑:同值取消,异值切换 const newValue = msg.feedback === value ? undefined : value; msg.feedback = newValue; // 更新 DOM 状态 const wrapper = messagesContainer$1.querySelector(`[data-csk-msg-id="${msgId}"]`); if (wrapper) updateFeedbackUI(wrapper, newValue); // 持久化(localStorage) if (config$1) saveMessages(config$1.integrateId, messages); // 调用后端 API 记录反馈 if (newValue) { const feedbackType = newValue === 'up' ? 'THUMBS_UP' : 'THUMBS_DOWN'; submitFeedbackApi(String(msgId), feedbackType).then(success => { if (success) { logger.info(`消息反馈已提交 msgId=${msgId} value=${newValue}`); } else { logger.warn(`消息反馈提交失败 msgId=${msgId}(本地状态已更新)`); } }); } else { logger.info(`消息反馈已取消 msgId=${msgId}`); } } function autoResizeInput() { if (!inputEl$1) return; inputEl$1.style.height = 'auto'; inputEl$1.style.height = `${Math.min(inputEl$1.scrollHeight, 120)}px`; } /** 根据消息数量切换欢迎空状态显隐 */ function updateEmptyState() { if (!welcomeEl) return; const hasMessages = messages.length > 0 || (messagesContainer$1 && messagesContainer$1.querySelector('.csk-msg')); welcomeEl.style.display = hasMessages ? 'none' : ''; } /** 更新发送按钮状态 */ 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'; // 1. 渲染用户气泡 const userTimestamp = now(); const userMsg = { id: uuid(), role: 'user', content: text, timestamp: userTimestamp }; if (messagesContainer$1) renderUserBubble(messagesContainer$1, text, userTimestamp); messages.push(userMsg); updateEmptyState(); if (clearBtn$1 && messages.length > 0) clearBtn$1.style.display = 'inline-flex'; if (messagesContainer$1) smartScrollToBottom(); // 2. 生成 AI 回复 await produceAIReply(text); } /** * 生成 AI 回复(发送请求 + 渲染气泡 + 持久化) * 复用于:正常发送、重试。调用方负责先渲染用户气泡。 */ async function produceAIReply(userText) { if (!config$1 || !messagesContainer$1) return; isSending = true; setSendButtonMode('stop'); // 确保 chatId 已初始化 if (!config$1.chatId) { await initChatId(); } const aiTimestamp = now(); // RAG 启用条件:由 enableRag 控制 const shouldUseRag = useRag; // 显示 loading if (showLoadingFn$1) showLoadingFn$1(); if (messagesContainer$1) smartScrollToBottom(); const aiMsgId = uuid(); let aiContent = ''; try { if (config$1.streaming) { aiContent = await sendStreamMessage(userText, aiTimestamp, shouldUseRag, aiMsgId); } else { aiContent = await chatRequest(userText); if (hideLoadingFn$1) hideLoadingFn$1(); if (messagesContainer$1) { renderAIBubble(messagesContainer$1, aiContent, aiTimestamp, renderMarkdown, aiMsgId); } } const aiMsg = { id: aiMsgId, role: 'ai', content: aiContent, timestamp: aiTimestamp }; messages.push(aiMsg); saveMessages(config$1.integrateId, messages); if (messagesContainer$1) smartScrollToBottom(); // a11y 播报新 AI 消息 announceMessage(aiContent); // 通知 launcher 显示未读徽章(弹窗关闭时生效,由 index.ts 监听) if (messagesContainer$1) { messagesContainer$1.dispatchEvent(new CustomEvent('csk:newMessage', { bubbles: true, detail: { msg: aiMsg } })); } // RAG 引用来源 if (shouldUseRag) fetchAndRenderSources(userText, 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; abortController = null; setSendButtonMode('send'); updateSendBtnState(); } } /** 流式发送消息 */ async function sendStreamMessage(text, aiTimestamp, shouldUseRag, aiMsgId) { // 创建中断控制器,供"停止生成"使用 abortController = new AbortController(); const signal = abortController.signal; return new Promise((resolve, reject) => { let bubbleEl = null; let wrapperEl = null; let accumulated = ''; let streamStarted = false; chatSSERequest(text, (chunk) => { accumulated += chunk; if (!streamStarted && messagesContainer$1) { if (hideLoadingFn$1) hideLoadingFn$1(); const { wrapper, bubble } = createEmptyAIBubble(messagesContainer$1, aiTimestamp, aiMsgId); wrapperEl = wrapper; bubbleEl = bubble; streamStarted = true; } if (bubbleEl) { bubbleEl.textContent = accumulated; appendStreamingCaret(bubbleEl); } if (messagesContainer$1) smartScrollToBottom(); }, () => { // 流结束 if (wrapperEl && bubbleEl) { // 无流内容降级为同步请求 if (!streamStarted && accumulated === '') { chatRequest(text).then(resolve).catch(reject); return; } if (accumulated) bubbleEl.innerHTML = renderMarkdown(accumulated); finalizeAIBubble(wrapperEl, bubbleEl); } resolve(accumulated); }, (error) => { if (accumulated.length > 0) { if (bubbleEl) { bubbleEl.innerHTML = renderMarkdown(accumulated + '\n\n' + t('stream_interrupted')); if (wrapperEl) finalizeAIBubble(wrapperEl, bubbleEl); } resolve(accumulated); } else { reject(error); } }, currentCategoryId, shouldUseRag, signal); }); } /** * 快捷问题发送:欢迎态点击芯片,直接以该文本发送 */ function sendQuickReply(text) { if (!inputEl$1 || isSending) return Promise.resolve(); inputEl$1.value = text; updateSendBtnState(); return handleSend(); } /** * 重试:根据 AI 消息 ID 重新生成该回复 * 找到该 AI 消息及其上一条用户消息,移除该 AI 消息(及之后的所有消息)后重新请求 */ async function retryFromMessage(msgId) { if (!config$1 || !messagesContainer$1 || isSending) return; const aiIndex = messages.findIndex(m => m.id === msgId && m.role === 'ai'); if (aiIndex < 0) return; // 找到上一条用户消息文本 let userText = ''; for (let i = aiIndex - 1; i >= 0; i--) { if (messages[i].role === 'user') { userText = messages[i].content; break; } } if (!userText) return; // 移除该 AI 消息及其后所有消息(本地 + DOM) const removed = messages.splice(aiIndex); const removedIds = new Set(removed.map(m => m.id)); const nodes = messagesContainer$1.querySelectorAll('.csk-msg'); nodes.forEach(node => { const id = node.dataset.cskMsgId; // 无 id 的(如错误气泡、被中断的)按顺序兜底:此处仅按 id 精确移除 if (id && removedIds.has(id)) node.remove(); }); saveMessages(config$1.integrateId, messages); updateEmptyState(); // 重新生成(不重复渲染用户气泡) await produceAIReply(userText); } /** 获取并渲染 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, msg.id); if (msg.sources && msg.sources.length > 0) renderSources(wrapper, msg.sources); if (msg.feedback) updateFeedbackUI(wrapper, msg.feedback); } } isNearBottom = true; scrollToBottom(messagesContainer$1); if (clearBtn$1 && messages.length > 0) clearBtn$1.style.display = 'inline-flex'; updateEmptyState(); 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'; updateEmptyState(); 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) { var _a; currentCategoryId = categoryId; useRag = (_a = config$1 === null || config$1 === void 0 ? void 0 : config$1.enableRag) !== null && _a !== void 0 ? _a : true; 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); historyItems = result.list.map(c => ({ id: c.conversationId || '', chatId: c.conversationId || '', messageCount: c.messageCount, lastMessageTime: c.lastMessageTime, lastMessagePreview: c.lastMessagePreview, createdAt: c.firstMessageTime || c.createdAt, })); // 重置搜索(新数据到达时清空过滤) historySearchText = ''; if (searchInput) searchInput.value = ''; renderHistoryList(listEl, historyItems, (conversationId) => { switchToConversation(conversationId); }, (id) => { window.open(getConversationExportUrl(id), '_blank'); }, async (id) => { if (!confirm(t('history_delete_confirm'))) return; const ok = await deleteConversation(id); if (ok) { if (id === getChatId()) { messages = []; if (messagesContainer$1) { messagesContainer$1.querySelectorAll('.csk-msg, .csk-loading').forEach(el => el.remove()); } if (clearBtn$1) clearBtn$1.style.display = 'none'; updateEmptyState(); } // 从缓存中移除并重新渲染(不再重新请求后端) historyItems = historyItems.filter(it => (it.chatId || it.id) !== id); renderFilteredHistory(); } }, getChatId()); } catch (err) { logger.error(t('history_load_error'), err); listEl.innerHTML = `
${t('history_load_error')}
`; } } /** * 切换到指定会话:加载上下文并继续对话 * @param conversationId 会话 ID(即 chatId) */ async function switchToConversation(conversationId) { if (!config$1 || !messagesContainer$1) return; logger.info(`切换到会话 conversationId=${conversationId}`); // 1. 更新 chatId updateChatId(conversationId); saveCachedChatId(config$1.integrateId, config$1.userId, conversationId); // 2. 关闭历史面板 if (historyPanel$1) { historyPanel$1.classList.add('csk-history-panel--hidden'); } // 3. 清空当前消息 messages = []; const msgs = messagesContainer$1.querySelectorAll('.csk-msg, .csk-loading'); msgs.forEach(el => el.remove()); // 4. 从后端加载该会话的消息 try { const result = await fetchConversationMessages(conversationId); if (result.messages.length > 0) { messages = result.messages.map((msg) => ({ id: uuid(), role: msg.messageType === 'USER' ? 'user' : 'ai', content: msg.content, timestamp: new Date(msg.createTime).getTime(), })); renderHistory(); logger.info(`加载会话 ${conversationId} 的 ${messages.length} 条消息`); // 同步到 localStorage saveMessages(config$1.integrateId, messages); } } catch (err) { logger.warn(`加载会话消息失败 conversationId=${conversationId}`, err); } // 5. 显示清空按钮 if (clearBtn$1 && messages.length > 0) { clearBtn$1.style.display = 'inline-flex'; } updateEmptyState(); } // ==================== 单例状态 ==================== 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; /** 提示气泡元素 */ let teaserEl = null; /** 提示气泡定时器 */ let teaserTimer = null; /** 未读徽章元素(挂在 launcher 内部) */ let badgeEl = null; /** 音频上下文(提示音用) */ let audioCtx = 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); // 3.1 设置错误回调 setErrorCallback(config.onError); // 4. 设置 API 配置 setApiConfig(config); // 5. 注入样式 injectStyles(config); // 6. 创建悬浮按钮 launcherEl = createLauncher(config, toggle); document.body.appendChild(launcherEl); // 获取未读徽章引用(由 createLauncher 创建) badgeEl = launcherEl.querySelector('.csk-launcher__badge'); // 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. 安置提示气泡(挂在 launcher 同层,由配置控制显示) teaserEl = dom.teaserEl; document.body.appendChild(teaserEl); positionTeaser(); // 9. 启用拖拽(含位置持久化) const headerEl = windowEl.querySelector('.csk-header'); if (headerEl) { dragCleanup = enableDrag(headerEl, windowEl, (pos) => { saveWindowPosition(config.integrateId, pos); }); } // 9.1 恢复上次拖拽位置 restoreWindowPosition(config.integrateId, windowEl); // 10. 初始化对话模块 initChat(config, { messagesContainer, inputEl, sendBtn, clearBtn, categorySelect, historyPanel, welcomeEl: dom.welcomeEl, newMsgBtn: dom.newMsgBtn, searchInput: dom.searchInput, ariaLiveEl: dom.ariaLiveEl, showLoading: showLoadingFn, hideLoading: hideLoadingFn, }); // 11. 监听知识库分类切换事件 windowEl.addEventListener('csk:categoryChange', ((e) => { setCategory(e.detail.categoryId); })); // 12. 监听会话管理面板加载事件 windowEl.addEventListener('csk:loadHistory', () => { loadHistoryConversations(); }); // 13. 监听快捷问题芯片点击 windowEl.addEventListener('csk:quickReply', ((e) => { sendQuickReply(e.detail.text); })); // 14. 监听 AI 消息重试事件 windowEl.addEventListener('csk:retry', ((e) => { retryFromMessage(e.detail.msgId); })); // 15. 监听消息反馈事件(👍 / 👎) windowEl.addEventListener('csk:feedback', ((e) => { handleFeedback(e.detail.msgId, e.detail.value); })); // 16. 新消息到达通知(用于未读徽章 + 提示音 + 桌面通知) windowEl.addEventListener('csk:newMessage', ((e) => { var _a; notifyNewMessage(); // 触发 onMessage 回调 if ((config === null || config === void 0 ? void 0 : config.onMessage) && ((_a = e.detail) === null || _a === void 0 ? void 0 : _a.msg)) { try { config.onMessage(e.detail.msg); } catch ( /* 回调异常不影响 SDK */_b) { /* 回调异常不影响 SDK */ } } })); // 17. ESC 键关闭弹窗 document.addEventListener('keydown', onKeyDown); // 17. 窗口 resize 时重新定位 teaser window.addEventListener('resize', onResize); isInitialized = true; logger.lifecycleInit(config.integrateId, config.requestDomain); // 触发 onReady 回调 if (config.onReady) { try { config.onReady(); } catch ( /* 回调异常不影响 SDK */_a) { /* 回调异常不影响 SDK */ } } // 18. 首访提示气泡(延迟显示,弹窗未打开时生效) if (config.showTeaser) { teaserTimer = setTimeout(() => { // 弹窗关闭时才显示 if (windowEl && windowEl.classList.contains('csk-window--hidden')) { showTeaser(); } }, 1500); } // 19. 异步初始化 chatId 和对话历史(不阻塞 UI) initChatHistory().catch(err => { logger.warn('chatId 初始化失败,将在发送消息时重试', err); }); } // ==================== 提示气泡控制 ==================== /** 根据 launcher 位置定位 teaser 气泡 */ function positionTeaser() { if (!teaserEl || !launcherEl) return; const rect = launcherEl.getBoundingClientRect(); const isLeft = (config === null || config === void 0 ? void 0 : config.position) === 'left-bottom'; if (isLeft) { teaserEl.style.left = `${rect.left}px`; } else { teaserEl.style.right = `${window.innerWidth - rect.right}px`; } teaserEl.style.bottom = `${window.innerHeight - rect.top + 8}px`; } function showTeaser() { if (!teaserEl) return; positionTeaser(); teaserEl.classList.remove('csk-teaser--hidden'); // 点击气泡本身 → 打开弹窗 teaserEl.addEventListener('click', onTeaserClick); // 关闭按钮(阻止冒泡,不打开弹窗) const closeBtn = teaserEl.querySelector('.csk-teaser__close'); if (closeBtn) { closeBtn.addEventListener('click', onTeaserClose); } } function hideTeaser() { if (!teaserEl) return; teaserEl.classList.add('csk-teaser--hidden'); teaserEl.removeEventListener('click', onTeaserClick); const closeBtn = teaserEl.querySelector('.csk-teaser__close'); if (closeBtn) closeBtn.removeEventListener('click', onTeaserClose); } function onTeaserClick(e) { // 关闭按钮点击不打开弹窗 if (e.target.closest('.csk-teaser__close')) return; hideTeaser(); open(); } function onTeaserClose(e) { e.stopPropagation(); hideTeaser(); } function onResize() { if (teaserEl && !teaserEl.classList.contains('csk-teaser--hidden')) { positionTeaser(); } } // ==================== 未读徽章 ==================== function showBadge() { if (badgeEl) badgeEl.classList.remove('csk-launcher__badge--hidden'); } function hideBadge() { if (badgeEl) badgeEl.classList.add('csk-launcher__badge--hidden'); } /** 新消息到达时弹窗关闭,则显示徽章 + 播放提示音 + 桌面通知 */ function notifyNewMessage() { if (windowEl && windowEl.classList.contains('csk-window--hidden')) { showBadge(); // 提示音 if (config === null || config === void 0 ? void 0 : config.sound) playNotificationSound(); // 桌面通知 if (config === null || config === void 0 ? void 0 : config.notification) sendDesktopNotification(); } } // ==================== a11y 焦点陷阱 ==================== /** 弹窗内可聚焦元素选择器 */ const FOCUSABLE = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; function trapFocus(e) { if (!windowEl || windowEl.classList.contains('csk-window--hidden')) return; if (e.key !== 'Tab') return; const focusables = Array.from(windowEl.querySelectorAll(FOCUSABLE)); if (focusables.length === 0) return; const first = focusables[0]; const last = focusables[focusables.length - 1]; if (e.shiftKey) { // Shift+Tab:在第一个元素上按 → 跳到最后一个 if (document.activeElement === first) { e.preventDefault(); last.focus(); } } else { // Tab:在最后一个元素上按 → 跳到第一个 if (document.activeElement === last) { e.preventDefault(); first.focus(); } } } // ==================== 全局键盘事件 ==================== function onKeyDown(e) { if (e.key === 'Escape' && windowEl && !windowEl.classList.contains('csk-window--hidden')) { close(); } trapFocus(e); } /** 销毁 SDK 实例 */ function destroy() { if (!isInitialized) return; if (teaserTimer) { clearTimeout(teaserTimer); teaserTimer = null; } hideTeaser(); if (teaserEl && teaserEl.parentNode) { teaserEl.parentNode.removeChild(teaserEl); teaserEl = null; } 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(); document.removeEventListener('keydown', onKeyDown); window.removeEventListener('resize', onResize); setErrorCallback(undefined); 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; badgeEl = null; audioCtx = null; logger.lifecycleDestroy(oldIntegrateId || ''); } function open() { if (!windowEl) return; windowEl.classList.remove('csk-window--hidden'); hideTeaser(); hideBadge(); setTimeout(() => { if (inputEl) inputEl.focus(); }, 80); } function close() { if (!windowEl) return; windowEl.classList.add('csk-window--hidden'); } function toggle() { if (!windowEl) return; if (windowEl.classList.contains('csk-window--hidden')) { open(); } else { close(); } } function clearHistory() { if (!config) return; if (clearBtn) { clearBtn.click(); } else if (confirm('确定清空所有对话记录?')) { clearMessages(config.integrateId); } } // ==================== 窗口位置记忆 ==================== /** localStorage key:窗口位置 */ function positionKey(integrateId) { return `csk_position_${integrateId}`; } /** 保存窗口拖拽位置到 localStorage */ function saveWindowPosition(integrateId, pos) { try { localStorage.setItem(positionKey(integrateId), JSON.stringify(pos)); } catch ( /* 忽略 */_a) { /* 忽略 */ } } /** 恢复窗口拖拽位置 */ function restoreWindowPosition(integrateId, winEl) { try { const raw = localStorage.getItem(positionKey(integrateId)); if (!raw) return; const pos = JSON.parse(raw); if (typeof pos.x !== 'number' || typeof pos.y !== 'number') return; // 校验位置在当前视口内 const maxX = window.innerWidth - winEl.offsetWidth; const maxY = window.innerHeight - winEl.offsetHeight; const x = Math.max(0, Math.min(pos.x, maxX)); const y = Math.max(0, Math.min(pos.y, maxY)); winEl.style.right = 'auto'; winEl.style.bottom = 'auto'; winEl.style.left = `${x}px`; winEl.style.top = `${y}px`; } catch ( /* 忽略 */_a) { /* 忽略 */ } } // ==================== 提示音 + 桌面通知 ==================== /** 播放短促提示音(Web Audio API 合成,无需音频文件) */ function playNotificationSound() { try { if (!audioCtx) { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } const ctx = audioCtx; if (ctx.state === 'suspended') { ctx.resume(); return; } // 合成两声短促"叮" const now = ctx.currentTime; [0, 0.15].forEach((offset, i) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.value = i === 0 ? 880 : 1100; gain.gain.setValueAtTime(0.15, now + offset); gain.gain.exponentialRampToValueAtTime(0.001, now + offset + 0.12); osc.connect(gain); gain.connect(ctx.destination); osc.start(now + offset); osc.stop(now + offset + 0.12); }); } catch ( /* 音频 API 不可用则忽略 */_a) { /* 音频 API 不可用则忽略 */ } } /** 发送桌面通知(需用户授权) */ function sendDesktopNotification() { try { if (!('Notification' in window)) return; if (Notification.permission === 'granted') { new Notification((config === null || config === void 0 ? void 0 : config.title) || 'AI 智能助手', { body: '收到新消息', icon: undefined, silent: true, }); } else if (Notification.permission !== 'denied') { // 首次请求权限 Notification.requestPermission(); } } catch ( /* 通知 API 不可用则忽略 */_a) { /* 通知 API 不可用则忽略 */ } } // ==================== 挂载到全局 ==================== const ChatbotSDK = { init, destroy, open, close, toggle, clearHistory, }; if (typeof window !== 'undefined') { window.ChatbotSDK = ChatbotSDK; } return ChatbotSDK; })(); //# sourceMappingURL=chatbot-sdk.js.map