本地 RAG 知识库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

834 lines
26 KiB

/**
* 对话核心模块 - 发送/接收/渲染
*
* 核心参数映射:
* 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,
submitFeedbackApi,
CskError,
} from './api';
import {
renderUserBubble,
renderAIBubble,
createEmptyAIBubble,
scrollToBottom,
renderSources,
renderHistoryList,
appendStreamingCaret,
finalizeAIBubble,
updateFeedbackUI,
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 inputWrap: HTMLElement | null = null;
let sendBtn: HTMLElement | null = null;
let clearBtn: HTMLElement | null = null;
let categorySelect: HTMLSelectElement | null = null;
let historyPanel: HTMLElement | null = null;
let welcomeEl: HTMLElement | null = null;
let newMsgBtn: HTMLElement | null = null;
let searchInput: HTMLInputElement | null = null;
let ariaLiveEl: HTMLElement | null = null;
let showLoadingFn: (() => HTMLElement) | null = null;
let hideLoadingFn: (() => void) | null = null;
let isSending = false;
/** 当前流式请求的中断控制器(用于"停止生成") */
let abortController: AbortController | null = null;
/** 用户是否停留在底部附近(用于智能滚动 + 新消息提示) */
let isNearBottom = true;
/** 缓存的历史会话列表(供搜索过滤用) */
let historyItems: HistoryItemData[] = [];
/** 当前历史搜索关键字 */
let historySearchText = '';
/** 当前选中的知识库分类 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;
welcomeEl: HTMLElement;
newMsgBtn: HTMLElement;
searchInput: HTMLInputElement | null;
ariaLiveEl: HTMLElement;
showLoading: () => HTMLElement;
hideLoading: () => void;
}
): void {
config = cfg;
messagesContainer = dom.messagesContainer;
inputEl = dom.inputEl;
inputWrap = dom.inputEl.parentElement;
sendBtn = dom.sendBtn;
clearBtn = dom.clearBtn;
categorySelect = dom.categorySelect;
historyPanel = dom.historyPanel;
welcomeEl = dom.welcomeEl;
newMsgBtn = dom.newMsgBtn;
searchInput = dom.searchInput;
ariaLiveEl = dom.ariaLiveEl;
showLoadingFn = dom.showLoading;
hideLoadingFn = dom.hideLoading;
// 初始化知识库分类
currentCategoryId = cfg.categoryId;
useRag = cfg.enableRag;
// 绑定发送事件
bindSendEvents();
// 绑定滚动监听(智能滚动 + 新消息提示)
bindScrollEvents();
// 绑定历史搜索过滤
bindHistorySearch();
// 加载知识库分类下拉框
if (cfg.showCategorySwitch && categorySelect) {
loadCategories();
}
}
/**
* 初始化 chatId 并加载对话历史
* 异步流程:查后端会话 → 恢复 chatId → 加载历史消息
*/
export async function initChatHistory(): Promise<void> {
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<void> {
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', () => {
if (isSending && abortController) {
abortController.abort();
return;
}
handleSend();
});
inputEl.addEventListener('keydown', (e: KeyboardEvent) => {
// IME 组合输入期间(中文/日文输入法选词回车)不触发发送
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
e.preventDefault();
handleSend();
}
});
inputEl.addEventListener('input', () => {
updateSendBtnState();
autoResizeInput();
});
// 输入框聚焦/失焦高亮容器
inputEl.addEventListener('focus', () => {
if (inputWrap) inputWrap.classList.add('csk-input-wrap--focus');
});
inputEl.addEventListener('blur', () => {
if (inputWrap) inputWrap.classList.remove('csk-input-wrap--focus');
});
if (clearBtn) {
clearBtn.addEventListener('click', () => handleClear());
}
}
/** 绑定滚动监听:判断是否在底部,控制新消息提示按钮 */
function bindScrollEvents(): void {
if (!messagesContainer) return;
messagesContainer.addEventListener('scroll', () => {
if (!messagesContainer) return;
const { scrollTop, scrollHeight, clientHeight } = messagesContainer;
// 距底部 80px 内视为"在底部"
isNearBottom = scrollHeight - scrollTop - clientHeight < 80;
if (isNearBottom) hideNewMsgBtn();
});
// 新消息提示按钮点击 → 回到底部
if (newMsgBtn) {
newMsgBtn.addEventListener('click', () => {
if (messagesContainer) scrollToBottom(messagesContainer);
isNearBottom = true;
hideNewMsgBtn();
});
}
}
/** 显示新消息提示按钮 */
function showNewMsgBtn(): void {
if (newMsgBtn) newMsgBtn.classList.remove('csk-newmsg--hidden');
}
/** 隐藏新消息提示按钮 */
function hideNewMsgBtn(): void {
if (newMsgBtn) newMsgBtn.classList.add('csk-newmsg--hidden');
}
/** 智能滚动到底部:用户在底部时自动滚,上滑时显示新消息提示 */
function smartScrollToBottom(): void {
if (!messagesContainer) return;
if (isNearBottom) {
scrollToBottom(messagesContainer);
} else {
showNewMsgBtn();
}
}
/** 切换发送按钮模式:发送 / 停止生成 */
function setSendButtonMode(mode: 'send' | 'stop'): void {
if (!sendBtn) return;
if (mode === 'stop') {
sendBtn.classList.add('csk-send-btn--stop');
sendBtn.removeAttribute('disabled');
sendBtn.setAttribute('title', t('stop'));
sendBtn.setAttribute('aria-label', t('stop'));
sendBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>`;
} else {
sendBtn.classList.remove('csk-send-btn--stop');
sendBtn.setAttribute('title', t('send'));
sendBtn.setAttribute('aria-label', t('send'));
sendBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>`;
updateSendBtnState();
}
}
// ==================== 历史会话搜索过滤 ====================
/** 绑定历史搜索输入事件 */
function bindHistorySearch(): void {
if (!searchInput) return;
searchInput.addEventListener('input', () => {
historySearchText = searchInput!.value.trim().toLowerCase();
renderFilteredHistory();
});
}
/** 按搜索关键字过滤并重新渲染历史列表 */
function renderFilteredHistory(): void {
const listEl = historyPanel?.querySelector('#csk-history-list') as HTMLElement | null;
if (!listEl || !config) 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: string) => { switchToConversation(conversationId); },
(id: string) => { window.open(getConversationExportUrl(id), '_blank'); },
async (id: string) => {
if (!confirm(t('history_delete_confirm'))) return;
const ok = await deleteConversation(id);
if (ok) {
if (id === getChatId()) {
messages = [];
if (messagesContainer) {
messagesContainer.querySelectorAll('.csk-msg, .csk-loading').forEach(el => el.remove());
}
if (clearBtn) clearBtn.style.display = 'none';
updateEmptyState();
}
// 从缓存中移除并重新渲染
historyItems = historyItems.filter(it => (it.chatId || it.id) !== id);
renderFilteredHistory();
}
},
getChatId()
);
}
// ==================== a11y 消息播报 ====================
/** 通过 aria-live 区域播报新消息(供屏幕阅读器使用) */
function announceMessage(text: string): void {
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 记录反馈
*/
export function handleFeedback(msgId: string, value: 'up' | 'down'): void {
if (!messagesContainer) 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.querySelector(`[data-csk-msg-id="${msgId}"]`) as HTMLElement;
if (wrapper) updateFeedbackUI(wrapper, newValue);
// 持久化(localStorage)
if (config) saveMessages(config.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(): void {
if (!inputEl) return;
inputEl.style.height = 'auto';
inputEl.style.height = `${Math.min(inputEl.scrollHeight, 120)}px`;
}
/** 根据消息数量切换欢迎空状态显隐 */
function updateEmptyState(): void {
if (!welcomeEl) return;
const hasMessages = messages.length > 0 ||
(messagesContainer && messagesContainer.querySelector('.csk-msg'));
welcomeEl.style.display = hasMessages ? 'none' : '';
}
/** 更新发送按钮状态 */
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<void> {
if (!inputEl || !config || isSending) return;
const text = inputEl.value.trim();
if (text === '') return;
inputEl.value = '';
updateSendBtnState();
inputEl.style.height = 'auto';
// 1. 渲染用户气泡
const userTimestamp = now();
const userMsg: ChatMessage = { id: uuid(), role: 'user', content: text, timestamp: userTimestamp };
if (messagesContainer) renderUserBubble(messagesContainer, text, userTimestamp);
messages.push(userMsg);
updateEmptyState();
if (clearBtn && messages.length > 0) clearBtn.style.display = 'inline-flex';
if (messagesContainer) smartScrollToBottom();
// 2. 生成 AI 回复
await produceAIReply(text);
}
/**
* 生成 AI 回复(发送请求 + 渲染气泡 + 持久化)
* 复用于:正常发送、重试。调用方负责先渲染用户气泡。
*/
async function produceAIReply(userText: string): Promise<void> {
if (!config || !messagesContainer) return;
isSending = true;
setSendButtonMode('stop');
// 确保 chatId 已初始化
if (!config.chatId) {
await initChatId();
}
const aiTimestamp = now();
// RAG 启用条件:由 enableRag 控制
const shouldUseRag = useRag;
// 显示 loading
if (showLoadingFn) showLoadingFn();
if (messagesContainer) smartScrollToBottom();
const aiMsgId = uuid();
let aiContent = '';
try {
if (config.streaming) {
aiContent = await sendStreamMessage(userText, aiTimestamp, shouldUseRag, aiMsgId);
} else {
aiContent = await chatRequest(userText);
if (hideLoadingFn) hideLoadingFn();
if (messagesContainer) {
renderAIBubble(messagesContainer, aiContent, aiTimestamp, renderMarkdown, aiMsgId);
}
}
const aiMsg: ChatMessage = { id: aiMsgId, role: 'ai', content: aiContent, timestamp: aiTimestamp };
messages.push(aiMsg);
saveMessages(config.integrateId, messages);
if (messagesContainer) smartScrollToBottom();
// a11y 播报新 AI 消息
announceMessage(aiContent);
// 通知 launcher 显示未读徽章(弹窗关闭时生效,由 index.ts 监听)
if (messagesContainer) {
messagesContainer.dispatchEvent(new CustomEvent('csk:newMessage', { bubbles: true, detail: { msg: aiMsg } }));
}
// RAG 引用来源
if (shouldUseRag) fetchAndRenderSources(userText, 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;
abortController = null;
setSendButtonMode('send');
updateSendBtnState();
}
}
/** 流式发送消息 */
async function sendStreamMessage(text: string, aiTimestamp: number, shouldUseRag: boolean, aiMsgId: string): Promise<string> {
// 创建中断控制器,供"停止生成"使用
abortController = new AbortController();
const signal = abortController.signal;
return new Promise((resolve, reject) => {
let bubbleEl: HTMLElement | null = null;
let wrapperEl: HTMLElement | null = null;
let accumulated = '';
let streamStarted = false;
chatSSERequest(
text,
(chunk: string) => {
accumulated += chunk;
if (!streamStarted && messagesContainer) {
if (hideLoadingFn) hideLoadingFn();
const { wrapper, bubble } = createEmptyAIBubble(messagesContainer, aiTimestamp, aiMsgId);
wrapperEl = wrapper;
bubbleEl = bubble;
streamStarted = true;
}
if (bubbleEl) {
bubbleEl.textContent = accumulated;
appendStreamingCaret(bubbleEl);
}
if (messagesContainer) 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: CskError) => {
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
);
});
}
/**
* 快捷问题发送:欢迎态点击芯片,直接以该文本发送
*/
export function sendQuickReply(text: string): Promise<void> {
if (!inputEl || isSending) return Promise.resolve();
inputEl.value = text;
updateSendBtnState();
return handleSend();
}
/**
* 重试:根据 AI 消息 ID 重新生成该回复
* 找到该 AI 消息及其上一条用户消息,移除该 AI 消息(及之后的所有消息)后重新请求
*/
export async function retryFromMessage(msgId: string): Promise<void> {
if (!config || !messagesContainer || 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.querySelectorAll('.csk-msg');
nodes.forEach(node => {
const id = (node as HTMLElement).dataset.cskMsgId;
// 无 id 的(如错误气泡、被中断的)按顺序兜底:此处仅按 id 精确移除
if (id && removedIds.has(id)) node.remove();
});
saveMessages(config.integrateId, messages);
updateEmptyState();
// 重新生成(不重复渲染用户气泡)
await produceAIReply(userText);
}
/** 获取并渲染 RAG 引用来源 */
async function fetchAndRenderSources(message: string, aiMsg: ChatMessage): Promise<void> {
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<void> {
if (!categorySelect) return;
try {
const tree = await fetchCategoryTree();
if (tree.length === 0) return;
categorySelect.innerHTML = `<option value="">${t('category_all')}</option>`;
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, msg.id);
if (msg.sources && msg.sources.length > 0) renderSources(wrapper, msg.sources);
if (msg.feedback) updateFeedbackUI(wrapper, msg.feedback);
}
}
isNearBottom = true;
scrollToBottom(messagesContainer);
if (clearBtn && messages.length > 0) clearBtn.style.display = 'inline-flex';
updateEmptyState();
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';
updateEmptyState();
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 = config?.enableRag ?? true;
logger.lifecycleCategoryChange(categoryId ?? '全部');
}
// ==================== 会话管理面板 ====================
/** 加载会话列表并渲染 */
export async function loadHistoryConversations(): Promise<void> {
if (!historyPanel || !config) return;
const listEl = historyPanel.querySelector('#csk-history-list') as HTMLElement;
if (!listEl) return;
listEl.innerHTML = `<div class="csk-history-panel__loading">加载中...</div>`;
try {
const result = await fetchConversationList(1, 50, config.userId, config.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: string) => { switchToConversation(conversationId); },
(id: string) => { window.open(getConversationExportUrl(id), '_blank'); },
async (id: string) => {
if (!confirm(t('history_delete_confirm'))) return;
const ok = await deleteConversation(id);
if (ok) {
if (id === getChatId()) {
messages = [];
if (messagesContainer) {
messagesContainer.querySelectorAll('.csk-msg, .csk-loading').forEach(el => el.remove());
}
if (clearBtn) clearBtn.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 = `<div class="csk-history-panel__empty"><div class="csk-history-panel__empty-icon">⚠</div><div>${t('history_load_error')}</div></div>`;
}
}
/**
* 切换到指定会话:加载上下文并继续对话
* @param conversationId 会话 ID(即 chatId)
*/
export async function switchToConversation(conversationId: string): Promise<void> {
if (!config || !messagesContainer) return;
logger.info(`切换到会话 conversationId=${conversationId}`);
// 1. 更新 chatId
updateChatId(conversationId);
saveCachedChatId(config.integrateId, config.userId, conversationId);
// 2. 关闭历史面板
if (historyPanel) {
historyPanel.classList.add('csk-history-panel--hidden');
}
// 3. 清空当前消息
messages = [];
const msgs = messagesContainer.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' as const,
content: msg.content,
timestamp: new Date(msg.createTime).getTime(),
}));
renderHistory();
logger.info(`加载会话 ${conversationId}${messages.length} 条消息`);
// 同步到 localStorage
saveMessages(config.integrateId, messages);
}
} catch (err) {
logger.warn(`加载会话消息失败 conversationId=${conversationId}`, err);
}
// 5. 显示清空按钮
if (clearBtn && messages.length > 0) {
clearBtn.style.display = 'inline-flex';
}
updateEmptyState();
}
/** 获取当前消息列表 */
export function getMessages(): ChatMessage[] {
return messages;
}