277 lines
7.7 KiB
JavaScript
Raw Normal View History

/**
*
* 在线阅读器引擎 · 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
};