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

277 lines
7.7 KiB
JavaScript
Raw Permalink 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.

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