/** * 对话核心模块 - 发送/接收/渲染 * * 核心参数映射: * integrateId → roleId(客服角色 ID) * userId → accountId(客户账号 ID) * chatId → 自动管理(从 /conversation/list 获取或自动生成) */ import { ResolvedConfig, ChatMessage, RagSource } from './types'; import { chatRequest, chatSSERequest, fetchCategoryTree, fetchRagSources, fetchConversationList, fetchConversationMessages, deleteConversation, getConversationExportUrl, initChatId, updateChatId, getChatId, saveCachedChatId, CskError, } from './api'; import { renderUserBubble, renderAIBubble, createEmptyAIBubble, scrollToBottom, renderSources, renderHistoryList, HistoryItemData, } from './dom'; import { saveMessages, loadMessages, clearMessages } from './storage'; import { renderMarkdown } from './markdown'; import { logger } from './logger'; import { t } from './i18n'; import { uuid, now } from './utils'; let config: ResolvedConfig | null = null; let messages: ChatMessage[] = []; let messagesContainer: HTMLElement | null = null; let inputEl: HTMLTextAreaElement | null = null; let sendBtn: HTMLElement | null = null; let clearBtn: HTMLElement | null = null; let categorySelect: HTMLSelectElement | null = null; let historyPanel: HTMLElement | null = null; let showLoadingFn: (() => HTMLElement) | null = null; let hideLoadingFn: (() => void) | null = null; let isSending = false; /** 当前选中的知识库分类 ID */ let currentCategoryId: number | undefined; /** 当前是否使用 RAG 对话 */ let useRag = false; /** * 初始化对话模块 */ export function initChat( cfg: ResolvedConfig, dom: { messagesContainer: HTMLElement; inputEl: HTMLTextAreaElement; sendBtn: HTMLElement; clearBtn: HTMLElement | null; categorySelect: HTMLSelectElement | null; historyPanel: HTMLElement; showLoading: () => HTMLElement; hideLoading: () => void; } ): void { config = cfg; messagesContainer = dom.messagesContainer; inputEl = dom.inputEl; sendBtn = dom.sendBtn; clearBtn = dom.clearBtn; categorySelect = dom.categorySelect; historyPanel = dom.historyPanel; showLoadingFn = dom.showLoading; hideLoadingFn = dom.hideLoading; // 初始化知识库分类 currentCategoryId = cfg.categoryId; useRag = !!cfg.categoryId || !!cfg.showCategorySwitch; // 绑定发送事件 bindSendEvents(); // 加载知识库分类下拉框 if (cfg.showCategorySwitch && categorySelect) { loadCategories(); } } /** * 初始化 chatId 并加载对话历史 * 异步流程:查后端会话 → 恢复 chatId → 加载历史消息 */ export async function initChatHistory(): Promise { if (!config || !messagesContainer) return; // 1. 初始化 chatId(从后端获取已有会话或自动生成) await initChatId(); // 2. 尝试从后端加载对话历史 await loadHistoryFromBackend(); // 3. 如果后端无历史,尝试从 localStorage 恢复 if (messages.length === 0) { const cached = loadMessages(config.integrateId); if (cached.length > 0) { messages = cached; renderHistory(); logger.info(`从本地缓存恢复 ${cached.length} 条消息`); } } } /** * 从后端加载对话历史 */ async function loadHistoryFromBackend(): Promise { if (!config || !messagesContainer) 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' as const, content: msg.content, timestamp: new Date(msg.createTime).getTime(), })); renderHistory(); logger.info(`从后端加载 ${messages.length} 条历史消息`); // 同步到 localStorage saveMessages(config.integrateId, messages); } } catch (err) { logger.warn('从后端加载历史消息失败', err); } } /** 绑定发送相关事件 */ function bindSendEvents(): void { if (!inputEl || !sendBtn) return; sendBtn.addEventListener('click', () => handleSend()); inputEl.addEventListener('keydown', (e: KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }); inputEl.addEventListener('input', () => updateSendBtnState()); if (clearBtn) { clearBtn.addEventListener('click', () => handleClear()); } } /** 更新发送按钮状态 */ function updateSendBtnState(): void { if (!sendBtn || !inputEl) return; const hasText = inputEl.value.trim().length > 0; if (hasText && !isSending) { sendBtn.removeAttribute('disabled'); } else { sendBtn.setAttribute('disabled', 'true'); } } /** 处理发送消息 */ async function handleSend(): Promise { if (!inputEl || !config || isSending) return; const text = inputEl.value.trim(); if (text === '') return; inputEl.value = ''; updateSendBtnState(); inputEl.style.height = 'auto'; isSending = true; updateSendBtnState(); // 确保 chatId 已初始化 if (!config.chatId) { await initChatId(); } // 1. 渲染用户气泡 const userTimestamp = now(); if (messagesContainer) renderUserBubble(messagesContainer, text, userTimestamp); const userMsg: ChatMessage = { id: uuid(), role: 'user', content: text, timestamp: userTimestamp }; messages.push(userMsg); if (clearBtn && messages.length > 0) clearBtn.style.display = 'inline-flex'; if (messagesContainer) scrollToBottom(messagesContainer); // 2. 显示 loading if (showLoadingFn) showLoadingFn(); if (messagesContainer) scrollToBottom(messagesContainer); // 3. 发送请求 try { let aiContent: string; const aiTimestamp = now(); const shouldUseRag = useRag && (currentCategoryId !== undefined || config.categoryId !== undefined); if (config.streaming) { aiContent = await sendStreamMessage(text, aiTimestamp, shouldUseRag); } else { aiContent = await chatRequest(text); } if (hideLoadingFn) hideLoadingFn(); if (!config.streaming && messagesContainer) { renderAIBubble(messagesContainer, aiContent, aiTimestamp, renderMarkdown); } const aiMsg: ChatMessage = { id: uuid(), role: 'ai', content: aiContent, timestamp: aiTimestamp }; messages.push(aiMsg); saveMessages(config.integrateId, messages); if (messagesContainer) scrollToBottom(messagesContainer); // RAG 引用来源 if (shouldUseRag) fetchAndRenderSources(text, aiMsg); } catch (err) { if (hideLoadingFn) hideLoadingFn(); const errMsg = err instanceof CskError ? err.message : t('error_send'); if (messagesContainer) { 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.appendChild(errorBubble); } logger.error(`发送失败 integrateId=${config.integrateId}`, err); } finally { isSending = false; updateSendBtnState(); } } /** 流式发送消息 */ async function sendStreamMessage(text: string, aiTimestamp: number, shouldUseRag: boolean): Promise { return new Promise((resolve, reject) => { let bubbleEl: HTMLElement | null = null; let accumulated = ''; let streamStarted = false; chatSSERequest( text, (chunk: string) => { accumulated += chunk; if (!streamStarted && messagesContainer) { if (hideLoadingFn) hideLoadingFn(); const { bubble } = createEmptyAIBubble(messagesContainer, aiTimestamp); bubbleEl = bubble; streamStarted = true; } if (bubbleEl) bubbleEl.textContent = accumulated; if (messagesContainer) scrollToBottom(messagesContainer); }, () => { if (!streamStarted && accumulated === '') { chatRequest(text).then(resolve).catch(reject); return; } if (bubbleEl && accumulated) bubbleEl.innerHTML = renderMarkdown(accumulated); resolve(accumulated); }, (error: CskError) => { if (accumulated.length > 0) { if (bubbleEl) bubbleEl.innerHTML = renderMarkdown(accumulated + '\n\n' + t('stream_interrupted')); resolve(accumulated); } else { reject(error); } }, currentCategoryId, shouldUseRag ); }); } /** 获取并渲染 RAG 引用来源 */ async function fetchAndRenderSources(message: string, aiMsg: ChatMessage): Promise { try { const sources = await fetchRagSources(message, currentCategoryId); if (sources.length > 0) { const ragSources: RagSource[] = sources.map(s => ({ documentId: s.documentId || '', title: s.title || '', sourceName: s.sourceName || '', chunkIndex: s.chunkIndex ?? 0, score: s.score ?? 0, snippet: s.snippet || '', })); aiMsg.sources = ragSources; if (messagesContainer) { const lastAiMsg = messagesContainer.querySelector('.csk-msg--ai:last-of-type'); if (lastAiMsg) renderSources(lastAiMsg as HTMLElement, ragSources); } if (config) saveMessages(config.integrateId, messages); } } catch (err) { logger.warn('获取引用来源失败', err); } } /** 加载知识库分类到下拉框 */ async function loadCategories(): Promise { if (!categorySelect) return; try { const tree = await fetchCategoryTree(); if (tree.length === 0) return; categorySelect.innerHTML = ``; const addOptions = (nodes: typeof tree, indent: number = 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!.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(): void { if (!messagesContainer) return; const historyPanelEl = messagesContainer.querySelector('.csk-history-panel'); const msgs = messagesContainer.querySelectorAll('.csk-msg, .csk-loading'); msgs.forEach(el => el.remove()); for (const msg of messages) { if (msg.role === 'user') { renderUserBubble(messagesContainer, msg.content, msg.timestamp); } else { const wrapper = renderAIBubble(messagesContainer, msg.content, msg.timestamp, renderMarkdown); if (msg.sources && msg.sources.length > 0) renderSources(wrapper, msg.sources); } } scrollToBottom(messagesContainer); if (clearBtn && messages.length > 0) clearBtn.style.display = 'inline-flex'; if (historyPanelEl && !messagesContainer.contains(historyPanelEl)) { messagesContainer.appendChild(historyPanelEl); } } /** 清空对话历史(生成新 chatId) */ function handleClear(): void { if (!config) return; if (!confirm(t('clear_confirm'))) return; messages = []; if (messagesContainer) { const msgs = messagesContainer.querySelectorAll('.csk-msg, .csk-loading'); msgs.forEach(el => el.remove()); } if (clearBtn) clearBtn.style.display = 'none'; clearMessages(config.integrateId); // 生成新的 chatId,开始新会话 const newId = generateNewChatId(); updateChatId(newId); saveCachedChatId(config.integrateId, config.userId, newId); logger.lifecycleClear(config.integrateId); logger.info(`新 chatId=${newId}`); } /** 生成新 chatId */ function generateNewChatId(): string { const random = typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID().substring(0, 8) : Math.random().toString(36).substring(2, 10); return `sdk_${Date.now()}_${random}`; } /** 设置当前知识库分类 */ export function setCategory(categoryId: number | undefined): void { currentCategoryId = categoryId; useRag = categoryId !== undefined; logger.lifecycleCategoryChange(categoryId ?? '全部'); } // ==================== 会话管理面板 ==================== /** 加载会话列表并渲染 */ export async function loadHistoryConversations(): Promise { if (!historyPanel || !config) return; const listEl = historyPanel.querySelector('#csk-history-list') as HTMLElement; if (!listEl) return; listEl.innerHTML = `
加载中...
`; try { const result = await fetchConversationList(1, 50, config.userId, config.integrateId); const items: HistoryItemData[] = result.list.map(c => ({ id: c.conversationId || c.chatId || '', chatId: c.conversationId || c.chatId || '', messageCount: c.messageCount, lastMessageTime: c.lastMessageTime, createdAt: c.firstMessageTime || c.createdAt, })); renderHistoryList( listEl, items, (id: string) => { window.open(getConversationExportUrl(id), '_blank'); }, async (id: string) => { if (!confirm(t('history_delete_confirm'))) return; const ok = await deleteConversation(id); if (ok) loadHistoryConversations(); } ); } catch (err) { logger.error(t('history_load_error'), err); listEl.innerHTML = `
${t('history_load_error')}
`; } } /** 获取当前消息列表 */ export function getMessages(): ChatMessage[] { return messages; }