/** * 章节搜索API * 搜索章节标题和内容,不返回用户敏感信息 */ import { NextResponse } from 'next/server' import fs from 'fs' import path from 'path' export async function GET(request: Request) { try { const { searchParams } = new URL(request.url) const keyword = searchParams.get('q') || '' if (!keyword || keyword.trim().length < 1) { return NextResponse.json({ success: true, results: [], total: 0, message: '请输入搜索关键词' }) } const searchTerm = keyword.trim().toLowerCase() // 读取章节数据 const dataPath = path.join(process.cwd(), 'public/book-chapters.json') const fileContent = fs.readFileSync(dataPath, 'utf-8') const chaptersData = JSON.parse(fileContent) // 读取书籍内容目录 const bookDir = path.join(process.cwd(), 'book') const results: any[] = [] // 遍历章节搜索 for (const chapter of chaptersData) { const titleMatch = chapter.title?.toLowerCase().includes(searchTerm) const idMatch = chapter.id?.toLowerCase().includes(searchTerm) const partMatch = chapter.partTitle?.toLowerCase().includes(searchTerm) // 尝试读取章节内容进行搜索 let contentMatch = false let matchedContent = '' // 兼容两种字段名: file 或 filePath const filePathField = chapter.filePath || chapter.file if (filePathField) { try { // 如果是绝对路径,直接使用;否则相对于项目根目录 const filePath = filePathField.startsWith('/') ? filePathField : path.join(process.cwd(), filePathField) if (fs.existsSync(filePath)) { const content = fs.readFileSync(filePath, 'utf-8') // 移除敏感信息(手机号、微信号等) const cleanContent = content .replace(/1[3-9]\d{9}/g, '***') // 手机号 .replace(/微信[::]\s*\S+/g, '微信:***') // 微信号 .replace(/QQ[::]\s*\d+/g, 'QQ:***') // QQ号 .replace(/邮箱[::]\s*\S+@\S+/g, '邮箱:***') // 邮箱 if (cleanContent.toLowerCase().includes(searchTerm)) { contentMatch = true // 提取匹配的上下文(前后50个字符) const lowerContent = cleanContent.toLowerCase() const matchIndex = lowerContent.indexOf(searchTerm) if (matchIndex !== -1) { const start = Math.max(0, matchIndex - 30) const end = Math.min(cleanContent.length, matchIndex + searchTerm.length + 50) matchedContent = (start > 0 ? '...' : '') + cleanContent.slice(start, end).replace(/\n/g, ' ') + (end < cleanContent.length ? '...' : '') } } } } catch (e) { // 文件读取失败,跳过内容搜索 } } if (titleMatch || idMatch || partMatch || contentMatch) { // 从标题中提取章节号(如 "1.1 荷包:..." -> "1.1") const sectionIdMatch = chapter.title?.match(/^(\d+\.\d+)\s/) const sectionId = sectionIdMatch ? sectionIdMatch[1] : chapter.id // 处理特殊ID let finalId = sectionId if (chapter.id === 'preface' || chapter.title?.includes('序言')) { finalId = 'preface' } else if (chapter.id === 'epilogue') { finalId = 'epilogue' } else if (chapter.id?.startsWith('appendix')) { finalId = chapter.id } // 判断是否免费章节 const freeIds = ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3'] const isFree = freeIds.includes(finalId) results.push({ id: finalId, // 使用提取的章节号 title: chapter.title, part: chapter.partTitle || chapter.part || '', chapter: chapter.chapterDir || chapter.chapter || '', isFree: isFree, matchType: titleMatch ? 'title' : (idMatch ? 'id' : (partMatch ? 'part' : 'content')), matchedContent: contentMatch ? matchedContent : '', // 格式化章节号 chapterLabel: formatChapterLabel(finalId, chapter.index) }) } } // 按匹配类型排序:标题匹配 > ID匹配 > 内容匹配 results.sort((a, b) => { const order = { title: 0, id: 1, content: 2 } return (order[a.matchType as keyof typeof order] || 2) - (order[b.matchType as keyof typeof order] || 2) }) return NextResponse.json({ success: true, results: results.slice(0, 20), // 最多返回20条 total: results.length, keyword: keyword }) } catch (error) { console.error('Search error:', error) return NextResponse.json({ success: false, error: '搜索失败', results: [] }, { status: 500 }) } } // 格式化章节标签 function formatChapterLabel(id: string, index?: number): string { if (!id) return '' if (id === 'preface') return '序言' if (id.startsWith('chapter-') && index) return `第${index}节` if (id.startsWith('appendix')) return '附录' if (id === 'epilogue') return '后记' // 处理 1.1, 3.2 这样的格式 const match = id.match(/^(\d+)\.(\d+)$/) if (match) { return `${match[1]}.${match[2]}` } return id }