277 lines
7.7 KiB
JavaScript
277 lines
7.7 KiB
JavaScript
/**
|
||
* ═══════════════════════════════════════════════════════════
|
||
* 在线阅读器引擎 · Reader Engine Service
|
||
* ═══════════════════════════════════════════════════════════
|
||
*
|
||
* 智库节点 Phase 2 · ZY-SVR-006
|
||
* 守护: 铸渊 · ICE-GL-ZY001
|
||
* 版权: 国作登字-2026-A-00037559
|
||
*
|
||
* 功能:
|
||
* - 书架管理(成员个人书架·引用式·不重复存储)
|
||
* - 阅读进度存档(自动保存到 JSON)
|
||
* - 阅读偏好设置(字体/主题/字号)
|
||
* - 阅读统计(今日阅读时长/总时长/阅读字数)
|
||
*
|
||
* ═══════════════════════════════════════════════════════════
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const DATA_DIR = path.join(__dirname, '..', 'data');
|
||
const PROGRESS_FILE = path.join(DATA_DIR, 'reading-progress.json');
|
||
const BOOKSHELF_FILE = path.join(DATA_DIR, 'bookshelves.json');
|
||
|
||
// 内存缓存 (使用 null-prototype 对象防止原型链污染)
|
||
let progressData = Object.create(null);
|
||
let bookshelfData = Object.create(null);
|
||
|
||
// ─── 初始化 ───
|
||
function init() {
|
||
if (!fs.existsSync(DATA_DIR)) {
|
||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||
}
|
||
loadProgress();
|
||
loadBookshelves();
|
||
}
|
||
|
||
function loadProgress() {
|
||
try {
|
||
if (fs.existsSync(PROGRESS_FILE)) {
|
||
const raw = JSON.parse(fs.readFileSync(PROGRESS_FILE, 'utf8'));
|
||
progressData = Object.create(null);
|
||
for (const key of Object.keys(raw)) {
|
||
if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
|
||
progressData[key] = raw[key];
|
||
}
|
||
}
|
||
}
|
||
} catch {
|
||
progressData = Object.create(null);
|
||
}
|
||
}
|
||
|
||
function saveProgress() {
|
||
try {
|
||
fs.writeFileSync(PROGRESS_FILE, JSON.stringify(progressData, null, 2), 'utf8');
|
||
} catch (err) {
|
||
console.error('[ReaderEngine] 保存阅读进度失败:', err.message);
|
||
}
|
||
}
|
||
|
||
function loadBookshelves() {
|
||
try {
|
||
if (fs.existsSync(BOOKSHELF_FILE)) {
|
||
const raw = JSON.parse(fs.readFileSync(BOOKSHELF_FILE, 'utf8'));
|
||
bookshelfData = Object.create(null);
|
||
for (const key of Object.keys(raw)) {
|
||
if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
|
||
bookshelfData[key] = raw[key];
|
||
}
|
||
}
|
||
}
|
||
} catch {
|
||
bookshelfData = Object.create(null);
|
||
}
|
||
}
|
||
|
||
function saveBookshelves() {
|
||
try {
|
||
fs.writeFileSync(BOOKSHELF_FILE, JSON.stringify(bookshelfData, null, 2), 'utf8');
|
||
} catch (err) {
|
||
console.error('[ReaderEngine] 保存书架失败:', err.message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 验证 ID 安全性(防止原型链污染)
|
||
*/
|
||
function sanitizeId(id) {
|
||
if (!id || typeof id !== 'string') return null;
|
||
if (id === '__proto__' || id === 'constructor' || id === 'prototype') return null;
|
||
return id;
|
||
}
|
||
|
||
/**
|
||
* 保存阅读进度
|
||
* @param {string} memberId - 成员ID
|
||
* @param {string} bookId - 书籍ID
|
||
* @param {object} progress - { chapter_index, scroll_position, percent }
|
||
*/
|
||
function saveReadingProgress(memberId, bookId, progress) {
|
||
if (!sanitizeId(memberId) || !sanitizeId(bookId)) {
|
||
throw new Error('memberId 和 bookId 为必填项且不能包含非法字符');
|
||
}
|
||
|
||
if (!Object.prototype.hasOwnProperty.call(progressData, memberId)) {
|
||
progressData[memberId] = {};
|
||
}
|
||
|
||
progressData[memberId][bookId] = {
|
||
chapter_index: progress.chapter_index || 0,
|
||
scroll_position: progress.scroll_position || 0,
|
||
percent: progress.percent || 0,
|
||
updated_at: new Date().toISOString()
|
||
};
|
||
|
||
saveProgress();
|
||
|
||
return progressData[memberId][bookId];
|
||
}
|
||
|
||
/**
|
||
* 获取阅读进度
|
||
*/
|
||
function getReadingProgress(memberId, bookId) {
|
||
if (!sanitizeId(memberId)) return null;
|
||
if (!Object.prototype.hasOwnProperty.call(progressData, memberId)) return null;
|
||
if (bookId) {
|
||
if (!sanitizeId(bookId)) return null;
|
||
return progressData[memberId][bookId] || null;
|
||
}
|
||
return progressData[memberId];
|
||
}
|
||
|
||
/**
|
||
* 添加书籍到书架
|
||
*/
|
||
function addToBookshelf(memberId, bookId, bookMeta) {
|
||
if (!sanitizeId(memberId) || !sanitizeId(bookId)) {
|
||
throw new Error('memberId 和 bookId 为必填项且不能包含非法字符');
|
||
}
|
||
|
||
if (!Object.prototype.hasOwnProperty.call(bookshelfData, memberId)) {
|
||
bookshelfData[memberId] = [];
|
||
}
|
||
|
||
// 防止重复添加
|
||
const exists = bookshelfData[memberId].find(b => b.book_id === bookId);
|
||
if (exists) {
|
||
return { added: false, message: '书籍已在书架中' };
|
||
}
|
||
|
||
bookshelfData[memberId].push({
|
||
book_id: bookId,
|
||
title: bookMeta.title || bookId,
|
||
author: bookMeta.author || '未知',
|
||
added_at: new Date().toISOString()
|
||
});
|
||
|
||
saveBookshelves();
|
||
|
||
return { added: true, message: '已添加到书架' };
|
||
}
|
||
|
||
/**
|
||
* 从书架移除
|
||
*/
|
||
function removeFromBookshelf(memberId, bookId) {
|
||
if (!sanitizeId(memberId) || !sanitizeId(bookId)) return { removed: false };
|
||
if (!Object.prototype.hasOwnProperty.call(bookshelfData, memberId)) return { removed: false };
|
||
|
||
const before = bookshelfData[memberId].length;
|
||
bookshelfData[memberId] = bookshelfData[memberId].filter(b => b.book_id !== bookId);
|
||
|
||
if (bookshelfData[memberId].length < before) {
|
||
saveBookshelves();
|
||
return { removed: true };
|
||
}
|
||
|
||
return { removed: false, message: '书籍不在书架中' };
|
||
}
|
||
|
||
/**
|
||
* 获取成员书架
|
||
*/
|
||
function getBookshelf(memberId) {
|
||
if (!sanitizeId(memberId)) return [];
|
||
const shelf = (Object.prototype.hasOwnProperty.call(bookshelfData, memberId) ? bookshelfData[memberId] : []) || [];
|
||
// 附加阅读进度
|
||
return shelf.map(book => ({
|
||
...book,
|
||
progress: (progressData[memberId] && progressData[memberId][book.book_id]) || null
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* 保存阅读偏好
|
||
*/
|
||
function savePreferences(memberId, prefs) {
|
||
if (!sanitizeId(memberId)) {
|
||
throw new Error('memberId 不能为空或包含非法字符');
|
||
}
|
||
|
||
if (!Object.prototype.hasOwnProperty.call(progressData, memberId)) {
|
||
progressData[memberId] = {};
|
||
}
|
||
|
||
progressData[memberId].__preferences = {
|
||
font_size: prefs.font_size || 16,
|
||
font_family: prefs.font_family || 'default',
|
||
theme: prefs.theme || 'dark',
|
||
line_height: prefs.line_height || 1.8,
|
||
updated_at: new Date().toISOString()
|
||
};
|
||
|
||
saveProgress();
|
||
|
||
return progressData[memberId].__preferences;
|
||
}
|
||
|
||
/**
|
||
* 获取阅读偏好
|
||
*/
|
||
function getPreferences(memberId) {
|
||
if (!sanitizeId(memberId)) {
|
||
return {
|
||
font_size: 16,
|
||
font_family: 'default',
|
||
theme: 'dark',
|
||
line_height: 1.8
|
||
};
|
||
}
|
||
if (!Object.prototype.hasOwnProperty.call(progressData, memberId) || !progressData[memberId].__preferences) {
|
||
return {
|
||
font_size: 16,
|
||
font_family: 'default',
|
||
theme: 'dark',
|
||
line_height: 1.8
|
||
};
|
||
}
|
||
return progressData[memberId].__preferences;
|
||
}
|
||
|
||
/**
|
||
* 获取阅读器统计
|
||
*/
|
||
function getStats() {
|
||
const memberCount = Object.keys(bookshelfData).length;
|
||
const totalBooks = Object.values(bookshelfData).reduce((sum, shelf) => sum + shelf.length, 0);
|
||
const progressCount = Object.values(progressData).reduce((sum, member) => {
|
||
return sum + Object.keys(member).filter(k => k !== '__preferences').length;
|
||
}, 0);
|
||
|
||
return {
|
||
total_members: memberCount,
|
||
total_bookshelf_books: totalBooks,
|
||
total_progress_records: progressCount
|
||
};
|
||
}
|
||
|
||
// 初始化
|
||
init();
|
||
|
||
module.exports = {
|
||
saveReadingProgress,
|
||
getReadingProgress,
|
||
addToBookshelf,
|
||
removeFromBookshelf,
|
||
getBookshelf,
|
||
savePreferences,
|
||
getPreferences,
|
||
getStats
|
||
};
|