guanghulab/server/novel-db/app/services/download-engine.js
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

502 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* ═══════════════════════════════════════════════════════════
* 下载引擎 · 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
};