/** * ═══════════════════════════════════════════ * 光湖智库 · 前端主脚本 v2.0 * guanghu.online · ZY-PROJ-006 * 守护: 铸渊 · ICE-GL-ZY001 * 版权: 国作登字-2026-A-00037559 * * v2.0: 邮箱登录 + 真实搜索下载 + Agent对话 + 登录态管理 * ═══════════════════════════════════════════ */ 'use strict'; const API_BASE = '/api'; const REQUEST_TIMEOUT_MS = 15000; let currentToken = localStorage.getItem('zhiku_token') || null; let currentEmail = localStorage.getItem('zhiku_email') || null; let codeCooldown = 0; let codeCooldownTimer = null; /* ═══ 星空背景 ═══ */ (function initStars() { const canvas = document.getElementById('starCanvas'); if (!canvas) return; const ctx = canvas.getContext('2d'); let stars = []; const STAR_COUNT = 200; function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; stars = Array.from({ length: STAR_COUNT }, () => ({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, r: Math.random() * 1.2 + 0.3, a: Math.random(), da: (Math.random() - 0.5) * 0.008 })); } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (const s of stars) { s.a += s.da; if (s.a > 1 || s.a < 0.1) s.da = -s.da; ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); ctx.fillStyle = `rgba(200,210,255,${s.a * 0.5})`; ctx.fill(); } requestAnimationFrame(draw); } resize(); window.addEventListener('resize', resize); draw(); })(); /* ═══ Header 滚动阴影 ═══ */ document.querySelector('.content')?.addEventListener('scroll', function() { const header = document.getElementById('header'); if (header) header.classList.toggle('scrolled', this.scrollTop > 10); }); /* ═══ 工具函数 ═══ */ function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str || ''; return div.innerHTML; } function showLoginMsg(text, type) { const el = document.getElementById('loginMsg'); if (!el) return; el.textContent = text; el.className = 'login-msg ' + (type || ''); } /* ═══════════════════════════════════════════ * 邮箱登录系统 · Email Auth * ═══════════════════════════════════════════ */ /** 发送验证码 */ async function loginSendCode() { const emailInput = document.getElementById('loginEmail'); const email = (emailInput?.value || '').trim(); if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { showLoginMsg('请输入有效的邮箱地址', 'err'); return; } const sendBtn = document.getElementById('loginSendBtn'); const sendBtn2 = document.getElementById('loginSendBtn2'); if (sendBtn) sendBtn.disabled = true; if (sendBtn2) sendBtn2.disabled = true; showLoginMsg('正在发送验证码...', 'info'); try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); const res = await fetch(`${API_BASE}/auth/send-code`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }), signal: controller.signal }); clearTimeout(timeout); if (!res.ok && res.status >= 500) { showLoginMsg('服务器暂时不可用,请稍后重试', 'err'); if (sendBtn) sendBtn.disabled = false; if (sendBtn2) sendBtn2.disabled = false; return; } let data; try { data = await res.json(); } catch (jsonErr) { console.error('[zhiku] send-code JSON parse failed:', jsonErr); showLoginMsg('服务器响应异常,请稍后重试', 'err'); if (sendBtn) sendBtn.disabled = false; if (sendBtn2) sendBtn2.disabled = false; return; } if (data.error) { showLoginMsg(data.message || '发送失败', 'err'); if (sendBtn) sendBtn.disabled = false; if (sendBtn2) sendBtn2.disabled = false; return; } showLoginMsg('验证码已发送,请查收邮件', 'ok'); // 显示验证码输入区 const codeRow = document.getElementById('loginCodeRow'); if (codeRow) codeRow.style.display = 'flex'; if (sendBtn) sendBtn.style.display = 'none'; const verifyBtn = document.getElementById('loginVerifyBtn'); if (verifyBtn) verifyBtn.style.display = 'block'; // 禁用邮箱输入 if (emailInput) emailInput.readOnly = true; // 60秒冷却倒计时 codeCooldown = 60; if (codeCooldownTimer) clearInterval(codeCooldownTimer); codeCooldownTimer = setInterval(() => { codeCooldown--; if (sendBtn2) { if (codeCooldown > 0) { sendBtn2.textContent = `${codeCooldown}s`; sendBtn2.disabled = true; } else { sendBtn2.textContent = '重新发送'; sendBtn2.disabled = false; clearInterval(codeCooldownTimer); codeCooldownTimer = null; } } }, 1000); // 自动聚焦验证码输入 document.getElementById('loginCode')?.focus(); } catch (err) { console.error('[zhiku] send-code fetch error:', err.name, err.message); let msg = '网络连接失败,请检查网络后重试'; if (err.name === 'AbortError') { msg = '请求超时,请检查网络后重试'; } else if (err.name === 'TypeError') { // Safari/iOS 连接失败时抛出 TypeError,错误信息不友好 msg = '服务器连接失败,请稍后重试'; } else if (err.message) { msg = '连接失败,请稍后重试'; } showLoginMsg(msg, 'err'); if (sendBtn) sendBtn.disabled = false; if (sendBtn2) sendBtn2.disabled = false; } } /** 验证码校验 → 登录 */ async function loginVerify() { const email = (document.getElementById('loginEmail')?.value || '').trim(); const code = (document.getElementById('loginCode')?.value || '').trim(); if (!email || !code) { showLoginMsg('请输入邮箱和验证码', 'err'); return; } if (!/^\d{6}$/.test(code)) { showLoginMsg('验证码为6位数字', 'err'); return; } const verifyBtn = document.getElementById('loginVerifyBtn'); if (verifyBtn) verifyBtn.disabled = true; showLoginMsg('验证中...', 'info'); try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); const res = await fetch(`${API_BASE}/auth/verify`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, code }), signal: controller.signal }); clearTimeout(timeout); let data; try { data = await res.json(); } catch (jsonErr) { console.error('[zhiku] verify JSON parse failed:', jsonErr); showLoginMsg('服务器响应异常,请稍后重试', 'err'); if (verifyBtn) verifyBtn.disabled = false; return; } if (data.error) { showLoginMsg(data.message || '验证失败', 'err'); if (verifyBtn) verifyBtn.disabled = false; return; } // 登录成功 currentToken = data.token; currentEmail = data.email || email; localStorage.setItem('zhiku_token', currentToken); localStorage.setItem('zhiku_email', currentEmail); showLoginMsg('登录成功 · 欢迎来到光湖智库', 'ok'); // 切换到主页面 setTimeout(() => { enterMainPage(); }, 600); } catch (err) { console.error('[zhiku] verify fetch error:', err.name, err.message); let msg = '网络连接失败,请检查网络后重试'; if (err.name === 'AbortError') { msg = '请求超时,请检查网络后重试'; } else if (err.name === 'TypeError') { msg = '服务器连接失败,请稍后重试'; } else if (err.message) { msg = '连接失败,请稍后重试'; } showLoginMsg(msg, 'err'); if (verifyBtn) verifyBtn.disabled = false; } } /** 退出登录 */ async function doLogout() { try { await fetch(`${API_BASE}/auth/logout`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${currentToken}` } }); } catch {} currentToken = null; currentEmail = null; localStorage.removeItem('zhiku_token'); localStorage.removeItem('zhiku_email'); // 重置登录表单 const emailInput = document.getElementById('loginEmail'); if (emailInput) { emailInput.value = ''; emailInput.readOnly = false; } const codeInput = document.getElementById('loginCode'); if (codeInput) codeInput.value = ''; const codeRow = document.getElementById('loginCodeRow'); if (codeRow) codeRow.style.display = 'none'; const sendBtn = document.getElementById('loginSendBtn'); if (sendBtn) { sendBtn.style.display = 'block'; sendBtn.disabled = false; } const verifyBtn = document.getElementById('loginVerifyBtn'); if (verifyBtn) { verifyBtn.style.display = 'none'; verifyBtn.disabled = false; } showLoginMsg('', ''); // 显示登录弹窗,隐藏主页面 document.getElementById('loginOverlay').style.display = 'flex'; document.getElementById('mainPage').style.display = 'none'; } /** 进入主页面 */ function enterMainPage() { document.getElementById('loginOverlay').style.display = 'none'; document.getElementById('mainPage').style.display = 'block'; // 更新用户栏 const userBarEmail = document.getElementById('userBarEmail'); if (userBarEmail) userBarEmail.textContent = currentEmail || '—'; // 加载健康状态 checkHealth(); } /** 校验已存储的会话 */ async function checkSession() { if (!currentToken) return false; try { const res = await fetch(`${API_BASE}/auth/session`, { headers: { 'Authorization': `Bearer ${currentToken}` } }); const data = await res.json(); if (!data.error && data.user) { currentEmail = data.user.email || currentEmail; localStorage.setItem('zhiku_email', currentEmail); return true; } } catch {} // Token 失效 currentToken = null; currentEmail = null; localStorage.removeItem('zhiku_token'); localStorage.removeItem('zhiku_email'); return false; } /* ═══ 验证码输入框回车确认 ═══ */ document.getElementById('loginCode')?.addEventListener('keydown', e => { if (e.key === 'Enter') loginVerify(); }); document.getElementById('loginEmail')?.addEventListener('keydown', e => { if (e.key === 'Enter') loginSendCode(); }); /* ═══════════════════════════════════════════ * API 健康检查 * ═══════════════════════════════════════════ */ async function checkHealth() { const el = document.getElementById('apiStatus'); try { const res = await fetch(`${API_BASE}/health`); const data = await res.json(); if (data.status === 'ok') { // 计算数据源在线状态 const sources = data.data_sources || []; const onlineSources = sources.filter(s => s.reachable); const sourceSummary = onlineSources.length + '/' + sources.length; el.textContent = `● API 在线 · ${data.uptime_human || ''} · 数据源 ${sourceSummary}`; el.className = 'api-status ok'; // 如果有数据源离线,改为警告色 if (onlineSources.length < sources.length) { el.className = 'api-status warn'; const offlineSources = sources.filter(s => !s.reachable).map(s => s.name).join(', '); el.title = `离线数据源: ${offlineSources}`; } const statBooks = document.getElementById('statBooks'); const statUptime = document.getElementById('statUptime'); const statSources = document.getElementById('statSources'); const statCos = document.getElementById('statCos'); if (statBooks) statBooks.textContent = data.books_count || 0; if (statUptime) statUptime.textContent = data.uptime_human || '-'; if (statSources) statSources.textContent = sourceSummary; if (statCos) statCos.textContent = data.cos_configured ? '已连接' : '未配置'; } else { throw new Error('not ok'); } } catch { if (el) { el.textContent = '● API 离线'; el.className = 'api-status err'; } } } setInterval(checkHealth, 30000); /* ═══════════════════════════════════════════ * 搜索功能 · 真实数据源搜索 * ═══════════════════════════════════════════ */ /** 构建数据源状态提示 HTML */ function buildSourceStatusHtml(sourceStatus) { if (!sourceStatus || !sourceStatus.length) return ''; const errorSources = sourceStatus.filter(s => s.status === 'error'); if (errorSources.length === 0) return ''; const tips = errorSources.map(s => { const name = escapeHtml(s.name); const err = s.error || ''; const errMsg = err.includes('timeout') ? '连接超时' : err.includes('ECONNREFUSED') ? '服务未启动' : err.includes('ENOTFOUND') ? 'DNS解析失败' : err.includes('ECONNRESET') ? '连接被重置' : '不可达'; return `${name}: ${errMsg}`; }).join(' '); return `
⚠️ 部分数据源离线: ${tips} · 仅本地书库可用
`; } async function doSearch() { const q = document.getElementById('searchInput').value.trim(); const resultDiv = document.getElementById('searchResults'); if (!q) { resultDiv.innerHTML = '
请输入书名或作者名
'; resultDiv.style.display = 'block'; return; } if (!currentToken) { resultDiv.innerHTML = '
请先登录后再搜索
'; resultDiv.style.display = 'block'; return; } resultDiv.innerHTML = '
🔍 正在搜索「' + escapeHtml(q) + '」...
'; resultDiv.style.display = 'block'; try { const res = await fetch(`${API_BASE}/search?q=${encodeURIComponent(q)}`, { headers: { 'Authorization': `Bearer ${currentToken}` } }); let data; try { data = await res.json(); } catch (jsonErr) { resultDiv.innerHTML = '
⚠️ 服务器响应异常,请稍后重试
'; return; } if (data.error) { if (data.code === 'TOKEN_EXPIRED' || data.code === 'TOKEN_INVALID' || data.code === 'NO_TOKEN') { resultDiv.innerHTML = '
⚠️ 登录已过期,请重新登录
'; setTimeout(() => doLogout(), 2000); return; } resultDiv.innerHTML = '
⚠️ ' + escapeHtml(data.message) + '
'; } else if (!data.results || data.results.length === 0) { // 空结果时展示数据源状态,让用户知道为什么搜不到 const statusHtml = buildSourceStatusHtml(data.source_status); const localInfo = (data.source_status || []).find(s => s.id === 'local'); const localTotal = localInfo ? localInfo.books_total || 0 : 0; resultDiv.innerHTML = statusHtml + '
' + (statusHtml ? '数据源离线时只能搜索本地书库(当前收录 ' + localTotal + ' 本)' : '未找到匹配书籍 · 可尝试其他关键词') + '
'; } else { const statusHtml = buildSourceStatusHtml(data.source_status); const sourceColors = { local: '#34d399', fanqie: '#fbbf24', qimao: '#a78bfa', shu69: '#fb923c' }; resultDiv.innerHTML = statusHtml + '
找到 ' + data.total + ' 个结果 · 来源: ' + (data.sources_queried || []).join(', ') + '
' + data.results.map(book => { const sourceColor = sourceColors[book.source] || '#94a7d0'; const sourceName = book.source_name || book.source; const hasFile = book.has_file; const downloadAttr = !hasFile && book.source_book_id ? ` data-source="${escapeHtml(book.source)}" data-bookid="${escapeHtml(book.source_book_id)}" data-title="${escapeHtml(book.title)}" data-author="${escapeHtml(book.author || '')}"` : ''; return `
📕 ${escapeHtml(book.title)}
${escapeHtml(book.author || '未知作者')} ${book.category ? ' · ' + escapeHtml(book.category) : ''} ${book.word_count ? ' · ' + (book.word_count > 10000 ? Math.round(book.word_count / 10000) + '万字' : book.word_count + '字') : ''}
${sourceName} ${hasFile ? '已收录' : ''}
${hasFile ? ` ` : (book.source_book_id ? ` ` : '') }
`; }).join(''); } } catch (err) { resultDiv.innerHTML = '
搜索失败: ' + escapeHtml(err.message) + '
'; } } document.getElementById('searchInput')?.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch(); }); /* ═══════════════════════════════════════════ * 下载功能 * ═══════════════════════════════════════════ */ /** 启动从数据源下载到COS */ async function startDownload(source, sourceBookId, title, author) { if (!currentToken) { alert('请先登录'); return; } // 确认下载 const sourceNames = { fanqie: '番茄小说', qimao: '七猫小说', shu69: '69书吧' }; if (!confirm(`确认下载「${title}」(${sourceNames[source] || source}) 到智库?`)) return; try { const res = await fetch(`${API_BASE}/download/start`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${currentToken}` }, body: JSON.stringify({ source, source_book_id: sourceBookId, title, author }) }); const data = await res.json(); if (data.error) { alert('下载失败: ' + data.message); return; } // 弹出进度弹窗 showDownloadProgress(data.task_id, title); } catch (err) { alert('下载请求失败: ' + err.message); } } /** 下载进度轮询 */ function showDownloadProgress(taskId, title) { const resultDiv = document.getElementById('searchResults'); const progressHtml = `
⬇️ 正在下载「${escapeHtml(title)}」
排队中...
`; // 在搜索结果顶部插入 resultDiv.insertAdjacentHTML('afterbegin', progressHtml); // 轮询进度 const pollInterval = setInterval(async () => { try { const res = await fetch(`${API_BASE}/download/status/${taskId}`, { headers: { 'Authorization': `Bearer ${currentToken}` } }); const data = await res.json(); const bar = document.getElementById(`dl-bar-${taskId}`); const status = document.getElementById(`dl-status-${taskId}`); if (bar) bar.style.width = (data.progress || 0) + '%'; if (status) status.textContent = data.message || data.status; if (data.status === 'completed') { clearInterval(pollInterval); const card = document.getElementById(`dl-${taskId}`); if (card) { card.classList.add('dl-done'); if (status) status.textContent = '✅ 下载完成 · 已收录到智库'; } } else if (data.status === 'failed') { clearInterval(pollInterval); if (status) status.textContent = '❌ ' + (data.message || '下载失败'); } } catch { // 网络异常时静默重试 } }, 2000); // 5分钟超时保护 setTimeout(() => clearInterval(pollInterval), 300000); } /** 本地已收录书籍下载 */ async function downloadLocal(bookId) { if (!currentToken) return; try { // 优先尝试本地下载(不依赖COS) const res = await fetch(`${API_BASE}/download/local/${encodeURIComponent(bookId)}`, { headers: { 'Authorization': `Bearer ${currentToken}` } }); // 如果返回的是文件流(text/plain),直接触发下载 const contentType = res.headers.get('content-type') || ''; if (contentType.includes('text/plain')) { const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); const disposition = res.headers.get('content-disposition') || ''; const filenameMatch = disposition.match(/filename\*=UTF-8''(.+)/); a.href = url; a.download = filenameMatch ? decodeURIComponent(filenameMatch[1]) : 'book.txt'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); return; } const data = await res.json(); if (data.error) { // 回退到旧版COS下载 const res2 = await fetch(`${API_BASE}/download/${encodeURIComponent(bookId)}`, { headers: { 'Authorization': `Bearer ${currentToken}` } }); const data2 = await res2.json(); if (data2.error) { alert(data2.message); } else if (data2.download_url) { window.open(data2.download_url, '_blank'); } } else if (data.download_url) { window.open(data.download_url, '_blank'); } } catch (err) { alert('下载失败: ' + err.message); } } /* ═══════════════════════════════════════════ * 在线阅读器 · Online Reader * ═══════════════════════════════════════════ */ let readerState = { bookId: '', source: '', sourceBookId: '', title: '', chapters: [], currentChapter: 0, fontSize: 18, theme: 'dark' // 'dark' | 'light' }; /** 打开在线阅读器 */ async function readBook(bookId) { if (!currentToken) { alert('请先登录后再阅读'); return; } // 解析 bookId 获取 source 和 source_book_id // bookId 格式: "fq-123456" 或 "qm-789" 或 "fanqie-123456" let source = ''; let sourceBookId = ''; if (bookId.startsWith('fq-')) { source = 'fanqie'; sourceBookId = bookId.replace('fq-', ''); } else if (bookId.startsWith('qm-')) { source = 'qimao'; sourceBookId = bookId.replace('qm-', ''); } else if (bookId.includes('-')) { const parts = bookId.split('-'); source = parts[0]; sourceBookId = parts.slice(1).join('-'); } else { source = 'fanqie'; sourceBookId = bookId; } readerState.bookId = bookId; readerState.source = source; readerState.sourceBookId = sourceBookId; readerState.currentChapter = 0; // 显示阅读器 document.getElementById('readerOverlay').style.display = 'flex'; document.getElementById('readerContent').innerHTML = '
📖 正在获取目录...
'; document.getElementById('readerChapterList').innerHTML = '
加载中...
'; document.getElementById('readerNav').style.display = 'none'; try { const res = await fetch(`${API_BASE}/reader/catalog/${encodeURIComponent(source)}/${encodeURIComponent(sourceBookId)}`, { headers: { 'Authorization': `Bearer ${currentToken}` } }); const data = await res.json(); if (data.error) { document.getElementById('readerContent').innerHTML = `
⚠️ ${escapeHtml(data.message)}
`; return; } readerState.chapters = data.chapters || []; readerState.title = (data.book && data.book.title) || '未知书名'; document.getElementById('readerBookTitle').textContent = readerState.title; if (readerState.chapters.length === 0) { document.getElementById('readerContent').innerHTML = '
📭 未找到章节目录
'; document.getElementById('readerChapterList').innerHTML = '
暂无章节
'; return; } // 渲染章节列表 renderChapterList(); // 加载第一章 loadChapter(0); } catch (err) { document.getElementById('readerContent').innerHTML = `
加载失败: ${escapeHtml(err.message)}
`; } } /** 渲染章节侧边栏 */ function renderChapterList() { const list = document.getElementById('readerChapterList'); list.innerHTML = readerState.chapters.map((ch, i) => `
${i + 1} ${escapeHtml(ch.title || '第' + (i + 1) + '章')}
` ).join(''); } /** 加载指定章节 */ async function loadChapter(index) { if (index < 0 || index >= readerState.chapters.length) return; readerState.currentChapter = index; const ch = readerState.chapters[index]; // 更新UI状态 document.getElementById('readerContent').innerHTML = '
📖 加载章节中...
'; document.getElementById('readerNav').style.display = 'flex'; document.getElementById('readerNavInfo').textContent = `${index + 1} / ${readerState.chapters.length}`; document.getElementById('readerPrevBtn').disabled = index === 0; document.getElementById('readerNextBtn').disabled = index === readerState.chapters.length - 1; // 更新章节列表高亮 const items = document.querySelectorAll('.reader-chapter-item'); items.forEach(el => el.classList.toggle('active', parseInt(el.dataset.index) === index)); // 滚动章节列表到可见位置 const activeItem = document.querySelector('.reader-chapter-item.active'); if (activeItem) activeItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); try { const params = new URLSearchParams({ source: ch.source || readerState.source, item_id: ch.item_id || '', book_id: readerState.bookId }); const res = await fetch(`${API_BASE}/reader/chapter?${params}`, { headers: { 'Authorization': `Bearer ${currentToken}` } }); const data = await res.json(); if (data.error) { document.getElementById('readerContent').innerHTML = `
⚠️ ${escapeHtml(data.message)}
`; return; } const content = data.content || '(空白章节)'; const title = ch.title || `第${index + 1}章`; document.getElementById('readerContent').innerHTML = `

${escapeHtml(title)}

` + `
${formatReaderText(content)}
`; // 滚动到顶部 document.getElementById('readerBody').scrollTop = 0; } catch (err) { document.getElementById('readerContent').innerHTML = `
加载失败: ${escapeHtml(err.message)}
`; } } function readerPrevChapter() { if (readerState.currentChapter > 0) loadChapter(readerState.currentChapter - 1); } function readerNextChapter() { if (readerState.currentChapter < readerState.chapters.length - 1) loadChapter(readerState.currentChapter + 1); } function closeReader() { document.getElementById('readerOverlay').style.display = 'none'; } function readerFontSmaller() { readerState.fontSize = Math.max(12, readerState.fontSize - 2); const textEl = document.querySelector('.reader-text'); if (textEl) textEl.style.fontSize = readerState.fontSize + 'px'; } function readerFontLarger() { readerState.fontSize = Math.min(32, readerState.fontSize + 2); const textEl = document.querySelector('.reader-text'); if (textEl) textEl.style.fontSize = readerState.fontSize + 'px'; } function toggleReaderTheme() { const container = document.querySelector('.reader-container'); const btn = document.getElementById('readerThemeBtn'); if (readerState.theme === 'dark') { readerState.theme = 'light'; container.classList.add('reader-light'); btn.textContent = '🌞'; } else { readerState.theme = 'dark'; container.classList.remove('reader-light'); btn.textContent = '🌙'; } } function formatReaderText(text) { // 将纯文本格式化为适合阅读的HTML return text .split('\n') .map(line => { line = line.trim(); if (!line) return ''; return `

${escapeHtml(line)}

`; }) .filter(Boolean) .join(''); } /* 阅读器键盘快捷键 */ document.addEventListener('keydown', (e) => { const overlay = document.getElementById('readerOverlay'); if (!overlay || overlay.style.display === 'none') return; if (e.key === 'Escape') closeReader(); if (e.key === 'ArrowLeft') readerPrevChapter(); if (e.key === 'ArrowRight') readerNextChapter(); }); /* ═══════════════════════════════════════════ * AI图书管理员Agent · 对话交互 * ═══════════════════════════════════════════ */ let agentSending = false; async function agentSend() { const input = document.getElementById('agentInput'); const msg = (input?.value || '').trim(); if (!msg || agentSending) return; if (!currentToken) { appendAgentMessage('bot', '请先登录后再和我对话 📚'); return; } // 显示用户消息 appendAgentMessage('user', msg); input.value = ''; agentSending = true; // 显示thinking状态 const thinkingId = appendAgentMessage('bot', '思考中...', true); try { const res = await fetch(`${API_BASE}/agent/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${currentToken}` }, body: JSON.stringify({ message: msg }) }); const data = await res.json(); // 移除thinking removeAgentMessage(thinkingId); if (data.error) { if (data.code === 'TOKEN_EXPIRED' || data.code === 'NO_TOKEN') { appendAgentMessage('bot', '登录已过期,请重新登录'); setTimeout(() => doLogout(), 2000); } else { appendAgentMessage('bot', '⚠️ ' + (data.message || 'Agent异常')); } } else { // 显示Agent回复 appendAgentMessage('bot', data.reply || '(无回复)'); // 处理工具调用结果 if (data.tool_results && data.tool_results.length > 0) { for (const tool of data.tool_results) { if (tool.type === 'search' && tool.results) { appendAgentSearchResults(tool.results); } if (tool.type === 'download') { appendAgentMessage('bot', `⬇️ 已创建下载任务: 「${tool.title}」(任务ID: ${tool.task_id})`); showDownloadProgress(tool.task_id, tool.title); } } } } } catch (err) { removeAgentMessage(thinkingId); appendAgentMessage('bot', '网络错误: ' + err.message); } agentSending = false; } let agentMsgCounter = 0; function appendAgentMessage(role, text, isThinking) { const container = document.getElementById('agentMessages'); if (!container) return; const id = 'amsg-' + (++agentMsgCounter); const div = document.createElement('div'); div.className = `agent-msg agent-msg-${role}${isThinking ? ' agent-msg-thinking' : ''}`; div.id = id; const content = document.createElement('div'); content.className = 'agent-msg-content'; content.innerHTML = isThinking ? '●●●' : formatAgentText(text); div.appendChild(content); container.appendChild(div); // 自动滚动到底部 container.scrollTop = container.scrollHeight; return id; } function removeAgentMessage(id) { const el = document.getElementById(id); if (el) el.remove(); } function formatAgentText(text) { // 增强 Markdown 渲染 · 书岚风格 let html = escapeHtml(text); // 分隔线 html = html.replace(/^---$/gm, '
'); // 引用块 html = html.replace(/^>\s*(.+)$/gm, '
$1
'); // 粗体 html = html.replace(/\*\*(.+?)\*\*/g, '$1'); // 斜体 html = html.replace(/\*(.+?)\*/g, '$1'); // 行内代码 html = html.replace(/`(.+?)`/g, '$1'); // 书名号高亮(限制匹配长度避免性能问题) html = html.replace(/《([^》]{1,100})》/g, '《$1》'); // 换行 html = html.replace(/\n/g, '
'); return html; } function appendAgentSearchResults(results) { const container = document.getElementById('agentMessages'); if (!container || !results.length) return; const div = document.createElement('div'); div.className = 'agent-msg agent-msg-bot'; div.innerHTML = `
📜 找到 ${results.length} 本
${results.slice(0, 8).map(book => { const sourceName = book.source_name || book.source; const sourceClass = book.source === 'fanqie' ? 'fanqie' : (book.source === 'qimao' ? 'qimao' : 'local'); const hasFile = book.has_file; // 操作按钮 · 搜到书后直接展示「在线阅读」和「下载」 let actionBtns = ''; if (hasFile) { actionBtns = ` 已收录`; } else if (book.source_book_id) { actionBtns = ` `; } return `
📖 ${escapeHtml(book.title)} ${escapeHtml(book.author || '')} · ${sourceName}${book.word_count ? ' · ' + book.word_count : ''}
${actionBtns}
`; }).join('')}
`; container.appendChild(div); container.scrollTop = container.scrollHeight; } /* Agent输入框事件 */ document.getElementById('agentInput')?.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); agentSend(); } }); /* ═══════════════════════════════════════════ * 初始化 · 自动恢复登录态 * ═══════════════════════════════════════════ */ (async function init() { if (currentToken) { const valid = await checkSession(); if (valid) { enterMainPage(); } else { // Token失效,显示登录 document.getElementById('loginOverlay').style.display = 'flex'; document.getElementById('mainPage').style.display = 'none'; } } else { document.getElementById('loginOverlay').style.display = 'flex'; document.getElementById('mainPage').style.display = 'none'; } })();