/** * ═══════════════════════════════════════════════════════════ * 下载引擎 · Download Engine Service * ═══════════════════════════════════════════════════════════ * * 智库节点 Phase 2 · ZY-SVR-006 * 守护: 铸渊 · ICE-GL-ZY001 * 版权: 国作登字-2026-A-00037559 * * 功能: * - 接收下载任务(书名+作者+平台) * - 任务队列管理(排队/执行/完成/失败) * - TXT 文件存储到本地 data/books/ * - 下载完成后自动触发分章引擎 * - 开源小说下载器数据库匹配(优先本地库匹配) * * 支持平台: * - qimao (七猫) * - fanqie (番茄) * - yuewen (阅文) * - jjwxc (晋江) * * 开源数据源: * - novel-downloader (GitHub: 404) * - FreeNovel (GitHub: cnbattle/novel) * - NovelHarvester (GitHub: unclezs/NovelHarvester) * - biqugen / zxcs (知轩藏书) * - 69shu / 笔趣阁系列 * * ═══════════════════════════════════════════════════════════ */ 'use strict'; const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const BOOKS_DIR = process.env.BOOKS_DIR || path.join(__dirname, '..', 'data', 'books'); const TASKS_FILE = path.join(__dirname, '..', 'data', 'download-tasks.json'); const SOURCES_FILE = path.join(__dirname, '..', 'data', 'book-sources.json'); const SUPPORTED_PLATFORMS = ['qimao', 'fanqie', 'yuewen', 'jjwxc']; // ═══════════════════════════════════════════════════════════ // 开源小说下载器数据库注册表 // ═══════════════════════════════════════════════════════════ const OPEN_SOURCE_REGISTRIES = [ { id: 'novel-downloader', name: 'novel-downloader', description: '全平台小说下载工具 · 支持多源聚合', url: 'https://github.com/search?q=novel+downloader&type=repositories', type: 'downloader', platforms: ['qimao', 'fanqie', 'yuewen', 'jjwxc', 'qidian'], status: 'registered', book_count_estimate: 0, last_checked: null }, { id: 'novel-harvester', name: 'NovelHarvester (uncle小说)', description: 'Java桌面端小说下载器 · 全平台采集', url: 'https://github.com/unclezs/NovelHarvester', type: 'downloader', platforms: ['qimao', 'fanqie', 'yuewen', 'qidian', 'biquge'], status: 'registered', book_count_estimate: 0, last_checked: null }, { id: 'free-novel', name: 'FreeNovel (cnbattle)', description: 'Go语言小说聚合 · 多源搜索下载', url: 'https://github.com/cnbattle/novel', type: 'aggregator', platforms: ['biquge', '69shu', 'shuquge', 'xbiquge'], status: 'registered', book_count_estimate: 0, last_checked: null }, { id: 'zxcs-archive', name: '知轩藏书数据库', description: '精校TXT小说归档 · 含大量完本', url: 'https://zxcs.info', type: 'archive', platforms: ['zxcs'], status: 'registered', book_count_estimate: 0, last_checked: null }, { id: 'biquge-series', name: '笔趣阁系列源', description: '笔趣阁及镜像站 · 免费小说资源', url: 'https://www.biquge.com', type: 'source', platforms: ['biquge', 'xbiquge', 'ibiquge'], status: 'registered', book_count_estimate: 0, last_checked: null }, { id: '69shu-archive', name: '69书吧数据库', description: '69书吧 · 大量TXT小说资源', url: 'https://www.69shu.com', type: 'source', platforms: ['69shu'], status: 'registered', book_count_estimate: 0, last_checked: null } ]; // 本地书源索引 (缓存已知可匹配的书籍列表) let bookSourceIndex = Object.create(null); function loadBookSources() { try { if (fs.existsSync(SOURCES_FILE)) { const raw = JSON.parse(fs.readFileSync(SOURCES_FILE, 'utf8')); bookSourceIndex = Object.create(null); if (raw && typeof raw === 'object' && !Array.isArray(raw)) { for (const key of Object.keys(raw)) { if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') { bookSourceIndex[key] = raw[key]; } } } } } catch { bookSourceIndex = Object.create(null); } } function saveBookSources() { try { const dir = path.dirname(SOURCES_FILE); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(SOURCES_FILE, JSON.stringify(bookSourceIndex, null, 2), 'utf8'); } catch (err) { console.error('[DownloadEngine] 保存书源索引失败:', err.message); } } /** * 搜索开源数据库匹配 * @param {string} bookName - 书名 * @param {string} author - 作者 * @returns {object} 匹配结果 */ function searchOpenSources(bookName, author) { const query = (bookName || '').trim().toLowerCase(); const authorQuery = (author || '').trim().toLowerCase(); const matches = []; // 1. 搜索本地书源索引(已缓存的书籍) for (const key of Object.keys(bookSourceIndex)) { const entry = bookSourceIndex[key]; if (!entry || typeof entry !== 'object') continue; const name = (entry.book_name || '').toLowerCase(); const auth = (entry.author || '').toLowerCase(); if (name.includes(query) || query.includes(name)) { if (!authorQuery || auth.includes(authorQuery) || authorQuery.includes(auth)) { matches.push({ source_id: entry.source_id || 'local', source_name: entry.source_name || '本地缓存', book_name: entry.book_name, author: entry.author, match_type: name === query ? 'exact' : 'partial', available: true, file_path: entry.file_path || null }); } } } // 2. 检查已下载的书籍 const localBooks = listBooks(); for (const book of localBooks) { const name = (book.book_name || '').toLowerCase(); if (name.includes(query) || query.includes(name)) { if (!authorQuery || (book.author || '').toLowerCase().includes(authorQuery)) { const alreadyMatched = matches.some(m => m.book_name === book.book_name && m.source_id === 'local-downloaded'); if (!alreadyMatched) { matches.push({ source_id: 'local-downloaded', source_name: '本地已下载', book_name: book.book_name, author: book.author, match_type: name === query ? 'exact' : 'partial', available: true, file_path: book.filename }); } } } } // 3. 返回注册的数据源列表(标注哪些可能有这本书) const potentialSources = OPEN_SOURCE_REGISTRIES.map(reg => ({ source_id: reg.id, source_name: reg.name, type: reg.type, status: reg.status, platforms: reg.platforms, url: reg.url, can_search: reg.status === 'connected', description: reg.description })); return { query: { book_name: bookName, author: author || '' }, local_matches: matches, local_match_count: matches.length, registered_sources: potentialSources, registered_source_count: potentialSources.length, recommendation: matches.length > 0 ? '本地已有匹配,建议直接使用' : '本地无匹配,将尝试从平台下载' }; } /** * 获取所有注册数据源信息 */ function getRegisteredSources() { return OPEN_SOURCE_REGISTRIES.map(reg => ({ id: reg.id, name: reg.name, description: reg.description, type: reg.type, url: reg.url, platforms: reg.platforms, status: reg.status, book_count_estimate: reg.book_count_estimate, last_checked: reg.last_checked })); } /** * 添加书源记录到本地索引 */ function addBookSource(bookName, author, sourceId, sourceName, filePath) { const key = `${(bookName || '').trim()}_${(author || '').trim()}`.toLowerCase(); bookSourceIndex[key] = { book_name: (bookName || '').trim(), author: (author || '').trim(), source_id: sourceId, source_name: sourceName, file_path: filePath || null, indexed_at: new Date().toISOString() }; saveBookSources(); } // 内存任务队列 let tasks = []; // ─── 初始化 ─── function init() { if (!fs.existsSync(BOOKS_DIR)) { fs.mkdirSync(BOOKS_DIR, { recursive: true }); } loadTasks(); } // ─── 持久化任务列表 ─── function saveTasks() { try { const dir = path.dirname(TASKS_FILE); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(TASKS_FILE, JSON.stringify(tasks, null, 2), 'utf8'); } catch (err) { console.error('[DownloadEngine] 保存任务列表失败:', err.message); } } function loadTasks() { try { if (fs.existsSync(TASKS_FILE)) { tasks = JSON.parse(fs.readFileSync(TASKS_FILE, 'utf8')); } } catch { tasks = []; } } /** * 创建下载任务 * @param {object} params - { book_name, author, platform, requested_by } * @returns {object} task */ function createTask({ book_name, author, platform, requested_by }) { if (!book_name || !platform) { throw new Error('book_name 和 platform 为必填项'); } if (!SUPPORTED_PLATFORMS.includes(platform)) { throw new Error(`不支持的平台: ${platform}。支持: ${SUPPORTED_PLATFORMS.join(', ')}`); } const taskId = `DL-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`; const task = { task_id: taskId, book_name: book_name.trim(), author: (author || '未知').trim(), platform, status: 'queued', requested_by: requested_by || 'system', created_at: new Date().toISOString(), updated_at: new Date().toISOString(), file_path: null, file_size: null, error: null }; tasks.push(task); saveTasks(); // 异步执行下载(模拟) processTask(taskId); return task; } /** * 处理下载任务(Phase 2 使用本地模拟 · Phase 3 接入真实下载器) */ async function processTask(taskId) { const task = tasks.find(t => t.task_id === taskId); if (!task) return; task.status = 'downloading'; task.updated_at = new Date().toISOString(); saveTasks(); try { // Phase 2: 创建占位 TXT 文件(模拟下载完成) // Phase 3: 接入真实下载器(七猫/番茄/阅文/晋江 API 或爬虫) const safeBookName = task.book_name.replace(/[<>:"/\\|?*]/g, '_'); const safeAuthor = task.author.replace(/[<>:"/\\|?*]/g, '_'); const filename = `${safeBookName}_${safeAuthor}_${task.platform}.txt`; const filePath = path.join(BOOKS_DIR, filename); // 模拟下载延时 await new Promise(resolve => setTimeout(resolve, 500)); // 生成占位内容 const content = [ `《${task.book_name}》`, `作者: ${task.author}`, `平台: ${task.platform}`, `下载时间: ${new Date().toISOString()}`, '', '─'.repeat(40), '', '第一章 开始', '', '这是一本好书。下载引擎已成功创建书籍记录。', '真实内容将在接入开源下载器后自动填充。', '', '─'.repeat(40), '', '第二章 世界', '', '世界很大,故事很长。', '每一章都是一个新的开始。', '', '─'.repeat(40), '', '第三章 结尾', '', '故事未完待续。', '智库节点 Phase 2 · 下载引擎占位内容。', '' ].join('\n'); fs.writeFileSync(filePath, content, 'utf8'); const stats = fs.statSync(filePath); task.status = 'completed'; task.file_path = filename; task.file_size = stats.size; task.updated_at = new Date().toISOString(); // 自动将下载的书籍加入书源索引 addBookSource(task.book_name, task.author, 'platform-' + task.platform, task.platform, filename); } catch (err) { task.status = 'failed'; task.error = err.message; task.updated_at = new Date().toISOString(); } saveTasks(); } /** * 获取任务列表 * @param {object} filter - { status, platform, limit } */ function listTasks({ status, platform, limit } = {}) { let result = [...tasks]; if (status) result = result.filter(t => t.status === status); if (platform) result = result.filter(t => t.platform === platform); result.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); if (limit) result = result.slice(0, limit); return result; } /** * 获取单个任务 */ function getTask(taskId) { return tasks.find(t => t.task_id === taskId) || null; } /** * 获取已下载书籍列表 */ function listBooks() { if (!fs.existsSync(BOOKS_DIR)) return []; return fs.readdirSync(BOOKS_DIR) .filter(f => f.endsWith('.txt')) .map(filename => { const filePath = path.join(BOOKS_DIR, filename); const stats = fs.statSync(filePath); // 从文件名解析信息 const parts = filename.replace('.txt', '').split('_'); return { filename, book_name: parts[0] || filename, author: parts[1] || '未知', platform: parts[2] || '未知', size: stats.size, created_at: stats.birthtime.toISOString() }; }) .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); } /** * 获取书籍内容 */ function getBookContent(filename) { const filePath = path.join(BOOKS_DIR, filename); if (!fs.existsSync(filePath)) return null; // 防止路径遍历 const resolvedPath = path.resolve(filePath); const resolvedBase = path.resolve(BOOKS_DIR); if (!resolvedPath.startsWith(resolvedBase)) return null; return fs.readFileSync(filePath, 'utf8'); } /** * 获取引擎统计 */ function getStats() { const books = listBooks(); const allTasks = listTasks(); return { total_books: books.length, total_tasks: allTasks.length, queued_tasks: allTasks.filter(t => t.status === 'queued').length, downloading: allTasks.filter(t => t.status === 'downloading').length, completed_tasks: allTasks.filter(t => t.status === 'completed').length, failed_tasks: allTasks.filter(t => t.status === 'failed').length, total_size_bytes: books.reduce((sum, b) => sum + b.size, 0), supported_platforms: SUPPORTED_PLATFORMS, registered_sources: OPEN_SOURCE_REGISTRIES.length, local_index_count: Object.keys(bookSourceIndex).length }; } // 初始化 init(); loadBookSources(); module.exports = { createTask, listTasks, getTask, listBooks, getBookContent, getStats, searchOpenSources, getRegisteredSources, addBookSource, SUPPORTED_PLATFORMS };