var CONFIG = { baseUrl: '', // API基础路径,留空则自动获取 chatUrl: 'https://aiyy.ccut.edu.cn/ai/h5/chat/wszs', // 非流式接口 streamUrl: 'https://aiyy.ccut.edu.cn/ai/h5/stream/wszs', // 流式接口 healthUrl: 'https://aiyy.ccut.edu.cn/ai/health', // 健康检查 timeout: 30000, // 请求超时(ms) streamTimeout: 120000, // 流式超时(ms) maxRetry: 2, // 最大重试次数 retryDelay: 1000 // 重试延迟基数(ms) }; (function(global) { 'use strict'; // ==================== 配置 ==================== var CFG = (global.EnrollFloatConfig || {}); var CONFIG = { robotImg: CFG.robotImg || 'https://aiyy.ccut.edu.cn/fdai/robot.png', botName: CFG.botName || '网数小助手', title: CFG.title || '信息化小助手', placeholder: CFG.placeholder || '请输入您想咨询的问题...', welcome: CFG.welcome || JSON.stringify("您好,我是信息化小助手,您在使用校园信息化服务遇到问题可以咨询我。\n").slice(1, -1) }; // 如果robotImg为空,尝试自动推断 if (!CONFIG.robotImg) { var scriptPath = (document.currentScript && document.currentScript.src) || ''; if (scriptPath) { var base = scriptPath.substring(0, scriptPath.lastIndexOf('/') + 1); CONFIG.robotImg = base.replace(/\/js\//, '/') + 'robot.png'; } else { CONFIG.robotImg = 'enrollment/robot.png'; } } var IMG_REGEX = /(https?:\/\/[^\s"')]+\.(?:jpg|jpeg|png|gif|webp|bmp|svg)(?:\?[^\s"')]*)?)/gi; // ==================== DOM ==================== var root = null; var trigger = null; var win = null; var messagesEl = null; var textarea = null; var sendBtn = null; // ==================== 状态 ==================== var state = { open: false, expanded: false, sending: false, aiBubble: null, aiRawText: '', greetingIdx: 0 }; var GREETINGS = [ ]; // ==================== 初始化 ==================== function init() { buildDOM(); bindEvents(); appendWelcome(); console.log('[EnrollFloat] 悬浮组件已初始化'); } function buildDOM() { root = document.createElement('div'); root.className = 'enroll-float-root'; root.innerHTML = // ---- 触发器(小机器人) ---- '
' + '
点我咨询
' + '
' + ' ' + escAttr(CONFIG.botName) + '' + '
' + '
' + // ---- 对话窗口 ---- '
' + ' ' + '
' + '
' + '
' + ' '+ '
' + '
' + ' ' + escHtml(CONFIG.title) + '' + ' ' + CONFIG.botName + '在线' + '
' + '
' + '
' + ' ' + ' ' + '
' + '
' + ' ' + '
' + ' ' + '
' + '
' + ' ' + ' ' + '
' + '
' + '
'; document.body.appendChild(root); trigger = document.getElementById('efTrigger'); win = document.getElementById('efWindow'); messagesEl = document.getElementById('efMessages'); textarea = document.getElementById('efTextarea'); sendBtn = document.getElementById('efSendBtn'); } // ==================== 事件绑定 ==================== function bindEvents() { // 点击触发器 → 打开窗口 trigger.addEventListener('click', openWindow); // 关闭按钮 → 关闭窗口 document.getElementById('efBtnClose').addEventListener('click', closeWindow); // 展开按钮 → 展开/还原 document.getElementById('efBtnExpand').addEventListener('click', toggleExpand); // 输入框 textarea.addEventListener('input', onInput); textarea.addEventListener('keydown', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); doSend(); } }); // 发送按钮 sendBtn.addEventListener('click', doSend); } function onInput() { sendBtn.disabled = !textarea.value.trim() || state.sending; autoResize(); } function autoResize() { textarea.style.height = 'auto'; textarea.style.height = Math.min(textarea.scrollHeight, 90) + 'px'; } // ==================== 窗口控制 ==================== function openWindow() { state.open = true; trigger.classList.add('hidden'); win.classList.add('show'); void win.offsetWidth; // 强制重排 win.classList.add('visible'); setTimeout(function() { textarea.focus(); }, 280); } function closeWindow() { state.open = false; state.expanded = false; win.classList.remove('visible', 'expanded'); setTimeout(function() { win.classList.remove('show'); trigger.classList.remove('hidden'); }, 260); } function toggleExpand() { state.expanded = !state.expanded; win.classList.toggle('expanded', state.expanded); // 展开后聚焦消息区方便滚动查看 if (state.expanded) messagesEl.scrollTop = messagesEl.scrollHeight; } // ==================== 欢迎消息 ==================== function appendWelcome() { var text = CONFIG.welcome || (''); // text = text.replace(/\\\\n/g, "\n"); text = text.replace(/\\n/g, '
'); var el = document.createElement('div'); el.className = 'ef-welcome'; el.innerHTML = '
' + text + '
'; messagesEl.appendChild(el); } // ==================== 发送 & 接收 ==================== function doSend() { var msg = textarea.value.trim(); if (!msg || state.sending) return; appendMsg(msg, 'user'); textarea.value = ''; onInput(); state.sending = true; state.aiBubble = null; state.aiRawText = ''; showTyping(); AiApi.chatStream(msg, { onMessage: function(text, isOver) { if (isOver) { removeTyping(); finishSending(); } else { appendAiChunk(text); } }, onSyncData: function(sid) { console.log('[EnrollFloat] Session:', sid); }, onError: function(err) { removeTyping(); showError((err && err.message) || '出错了,请重试'); finishSending(); } }).catch(function(e) { removeTyping(); showError(((e && e.error) || '网络连接异常')); finishSending(); }); } function finishSending() { state.sending = false; sendBtn.disabled = !textarea.value.trim(); } // ==================== 消息渲染 ==================== /** 滚动到最新 */ function scrollToBottom() { requestAnimationFrame(function() { requestAnimationFrame(function() { messagesEl.scrollTo({ top: messagesEl.scrollHeight, behavior: 'smooth' }); }); }); } function appendMsg(text, role) { var row = document.createElement('div'); row.className = 'ef-msg-row' + (role === 'user' ? ' user' : ''); var isUser = role === 'user'; if (isUser) { row.innerHTML = '
👤
' + '
' + escHtml(text) + '
'; } else { row.innerHTML = '
' + '
' + text + '
'; } messagesEl.appendChild(row); scrollToBottom(); return row; } function appendAiChunk(text) { if (!state.aiBubble) { var row = appendMsg('', 'ai'); var bubble = row.querySelector('.ef-bubble'); bubble.innerHTML = '
'; state.aiBubble = bubble.querySelector('.ef-md-body'); state.aiRawText = ''; } state.aiRawText += text; try { state.aiBubble.innerHTML = Marked.parse(state.aiRawText) || ''; processImages(state.aiBubble); } catch (e) { state.aiBubble.textContent = state.aiRawText; } scrollToBottom(); } function showTyping() { var row = document.createElement('div'); row.className = 'ef-msg-row'; row.id = 'efTypingRow'; row.innerHTML = '
' + '
'; messagesEl.appendChild(row); scrollToBottom(); } function removeTyping() { var el = document.getElementById('efTypingRow'); if (el) el.remove(); } function showError(msg) { var row = document.createElement('div'); row.className = 'ef-msg-row'; row.innerHTML = '
⚠️
' + '
❌ ' + escHtml(msg) + '
'; messagesEl.appendChild(row); scrollToBottom(); state.aiBubble = null; } // 打招呼(点击机器人头像时) function sayHi() { var g = GREETINGS[state.greetingIdx % GREETINGS.length]; state.greetingIdx++; if (!state.open) openWindow(); setTimeout(function() { appendMsg(g, 'ai'); }, 100); } // ==================== 图片处理 ==================== function processImages(containerEl) { if (!containerEl) return; // 裸URL图片 → img标签 var walker = document.createTreeWalker(containerEl, NodeFilter.SHOW_TEXT, null, false); var nodes = []; while (walker.nextNode()) nodes.push(walker.currentNode); nodes.forEach(function(node) { var txt = node.textContent; if (!IMG_REGEX.test(txt)) return; var frag = document.createDocumentFragment(), lastIdx = 0; txt.replace(IMG_REGEX, function(m, url, off) { if (off > lastIdx) frag.appendChild(document.createTextNode(txt.substring(lastIdx, off))); frag.appendChild(createImg(url.trim())); lastIdx = off + m.length; return m; }); if (lastIdx < txt.length) frag.appendChild(document.createTextNode(txt.substring(lastIdx))); node.parentNode.replaceChild(frag, node); }); // → 还原为 containerEl.querySelectorAll('a').forEach(function(a) { var h = (a.getAttribute('href') || '').trim(); if (h && IMG_REGEX.test(h)) { var img = createImg(h); img.alt = a.textContent.trim() || '图片'; a.parentNode.replaceChild(img, a); } }); // 已有统一样式 containerEl.querySelectorAll('img').forEach(function(img) { if (img.classList.contains('ef-img')) return; styleImg(img); }); } function createImg(src) { var img = document.createElement('img'); img.src = src; img.alt = '图片'; img.loading = 'lazy'; styleImg(img); return img; } function styleImg(img) { img.className = 'ef-img'; img.onerror = function() { this.style.cssText = 'min-height:60px;background:#f1f5f9;display:flex;align-items:center;'; this.alt = '图片加载失败'; }; img.onclick = function(e) { e.preventDefault(); preview(this.src); }; } function preview(src) { hidePreview(); var ov = document.createElement('div'); ov.className = 'ef-img-overlay'; ov.id = 'efImgOv'; var img = document.createElement('img'); img.src = src; ov.appendChild(img); ov.onclick = hidePreview; document.body.appendChild(ov); } function hidePreview() { var el = document.getElementById('efImgOv'); if (el) el.remove(); } // ==================== 工具函数 ==================== function escHtml(t) { var d=document.createElement('div'); d.textContent=t; return d.innerHTML; } function escAttr(t) { return String(t).replace(/&/g,'&').replace(/"/g,'"').replace(/'/g,''').replace(//g,'>'); } // ==================== 启动 ==================== if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })(window);