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

409 lines
11 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.

/**
* ═══════════════════════════════════════════════════════════
* 智能分章引擎 · Chapter Engine Service
* ═══════════════════════════════════════════════════════════
*
* 智库节点 Phase 2 · ZY-SVR-006
* 守护: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* 功能:
* - 识别 TXT 章节分隔规律(正则匹配多种格式)
* - 无章节标识时按内容结构分段(空行/字数阈值)
* - 输出整齐 TXT + 目录索引 JSON
* - 支持重新分章(更新索引)
*
* 章节识别规则(优先级从高到低):
* 1. 标准格式: "第X章 标题" / "第X回 标题"
* 2. 数字格式: "Chapter N" / "章节N"
* 3. 简写格式: "1." / "001" 开头的行
* 4. 分隔符: "──" / "***" / "===" 等分隔线
* 5. 兜底: 按空行+字数阈值切分
*
* ═══════════════════════════════════════════════════════════
*/
'use strict';
const fs = require('fs');
const path = require('path');
const BOOKS_DIR = process.env.BOOKS_DIR || path.join(__dirname, '..', 'data', 'books');
const CHAPTERS_DIR = process.env.CHAPTERS_DIR || path.join(__dirname, '..', 'data', 'chapters');
// ─── 章节匹配正则 ───
const CHAPTER_PATTERNS = [
// 第X章/回/节/篇/卷 (支持中文数字和阿拉伯数字)
/^第[一二三四五六七八九十百千万\d]+[章回节篇卷]\s*.*/,
// Chapter N / CHAPTER N
/^[Cc]hapter\s+\d+/,
// 章节N / 章N
/^章节?\s*\d+/,
// 序章/楔子/尾声/番外
/^(序章|楔子|引子|尾声|番外|后记|前言|附录)\s*/,
// 纯数字行(如 "001" 或 "1."
/^\d{1,4}[.、.]\s*.+/
];
// 分隔符模式
const SEPARATOR_PATTERN = /^[─━═\-\*]{5,}$/;
// 初始化目录
function init() {
if (!fs.existsSync(CHAPTERS_DIR)) {
fs.mkdirSync(CHAPTERS_DIR, { recursive: true });
}
}
/**
* 验证 ID 安全性(防止路径遍历和原型链污染)
*/
function sanitizeId(id) {
if (!id || typeof id !== 'string') return null;
// 禁止原型链污染
if (id === '__proto__' || id === 'constructor' || id === 'prototype') return null;
// 禁止路径遍历字符
if (id.includes('..') || id.includes('/') || id.includes('\\')) return null;
return id;
}
/**
* 对一本书进行分章
* @param {string} filename - 书籍文件名 (在 data/books/ 下)
* @returns {object} { book_id, title, total_chapters, chapters: [...], toc: [...] }
*/
function splitChapters(filename) {
if (!sanitizeId(filename)) {
throw new Error('非法文件名');
}
const filePath = path.join(BOOKS_DIR, filename);
// 防止路径遍历
const resolvedPath = path.resolve(filePath);
const resolvedBase = path.resolve(BOOKS_DIR);
if (!resolvedPath.startsWith(resolvedBase)) {
throw new Error('非法路径');
}
if (!fs.existsSync(filePath)) {
throw new Error(`书籍文件不存在: ${filename}`);
}
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split('\n');
// 提取书名(文件名或第一行)
const bookId = filename.replace('.txt', '');
const bookMeta = extractBookMeta(lines);
// 尝试按章节标识分割
let chapters = splitByPatterns(lines);
// 如果没有找到章节标识,按分隔符分割
if (chapters.length <= 1) {
chapters = splitBySeparators(lines);
}
// 如果仍然只有一章,按空行+字数阈值切分
if (chapters.length <= 1) {
chapters = splitByParagraphs(lines);
}
// 构建目录索引
const toc = chapters.map((ch, idx) => ({
index: idx,
title: ch.title,
start_line: ch.start_line,
char_count: ch.content.length,
word_count: ch.content.replace(/\s/g, '').length
}));
// 保存分章结果
const result = {
book_id: bookId,
title: bookMeta.title || bookId,
author: bookMeta.author || '未知',
total_chapters: chapters.length,
total_chars: content.length,
split_method: chapters.length > 1 ? detectMethod(chapters) : 'single',
created_at: new Date().toISOString(),
toc,
chapters: chapters.map((ch, idx) => ({
index: idx,
title: ch.title,
content: ch.content
}))
};
// 保存到 chapters 目录
const chapterDir = path.join(CHAPTERS_DIR, bookId);
const resolvedChapterDir = path.resolve(chapterDir);
const resolvedChaptersBase = path.resolve(CHAPTERS_DIR);
if (!resolvedChapterDir.startsWith(resolvedChaptersBase)) {
throw new Error('非法路径');
}
if (!fs.existsSync(chapterDir)) {
fs.mkdirSync(chapterDir, { recursive: true });
}
// 保存索引
const indexFilePath = path.join(chapterDir, 'index.json');
fs.writeFileSync(
indexFilePath,
JSON.stringify({ ...result, chapters: undefined }, null, 2),
'utf8'
);
// 保存每章内容
for (let i = 0; i < chapters.length; i++) {
const chFilePath = path.join(chapterDir, `chapter-${String(i).padStart(4, '0')}.txt`);
fs.writeFileSync(chFilePath, chapters[i].content, 'utf8');
}
return result;
}
/**
* 提取书籍元信息(从前几行)
*/
function extractBookMeta(lines) {
const meta = { title: '', author: '' };
const header = lines.slice(0, 10).join('\n');
// 匹配书名《xxx》
const titleMatch = header.match(/《(.+?)》/);
if (titleMatch) meta.title = titleMatch[1];
// 匹配作者
const authorMatch = header.match(/作者[:]\s*(.+)/);
if (authorMatch) meta.author = authorMatch[1].trim();
return meta;
}
/**
* 按章节正则模式分割
*/
function splitByPatterns(lines) {
const chapters = [];
let currentChapter = null;
let lineNum = 0;
for (const line of lines) {
lineNum++;
const trimmed = line.trim();
// 检查是否匹配章节模式
const isChapterHead = CHAPTER_PATTERNS.some(p => p.test(trimmed));
if (isChapterHead) {
if (currentChapter) {
chapters.push(currentChapter);
}
currentChapter = {
title: trimmed,
start_line: lineNum,
content: ''
};
} else if (currentChapter) {
currentChapter.content += line + '\n';
}
}
if (currentChapter) {
chapters.push(currentChapter);
}
return chapters;
}
/**
* 按分隔符分割
*/
function splitBySeparators(lines) {
const chapters = [];
let currentContent = '';
let chapterNum = 0;
let startLine = 1;
for (let i = 0; i < lines.length; i++) {
const trimmed = lines[i].trim();
if (SEPARATOR_PATTERN.test(trimmed) && currentContent.trim().length > 50) {
chapterNum++;
chapters.push({
title: `段落 ${chapterNum}`,
start_line: startLine,
content: currentContent.trim()
});
currentContent = '';
startLine = i + 2;
} else {
currentContent += lines[i] + '\n';
}
}
// 最后一段
if (currentContent.trim().length > 0) {
chapterNum++;
chapters.push({
title: `段落 ${chapterNum}`,
start_line: startLine,
content: currentContent.trim()
});
}
return chapters;
}
/**
* 按空行+字数阈值切分(兜底方案)
*/
function splitByParagraphs(lines, threshold = 3000) {
const chapters = [];
let currentContent = '';
let chapterNum = 0;
let startLine = 1;
let consecutiveEmpty = 0;
for (let i = 0; i < lines.length; i++) {
const trimmed = lines[i].trim();
if (trimmed === '') {
consecutiveEmpty++;
} else {
consecutiveEmpty = 0;
}
currentContent += lines[i] + '\n';
// 当连续空行≥2且已超过阈值时切分
if (consecutiveEmpty >= 2 && currentContent.trim().length >= threshold) {
chapterNum++;
chapters.push({
title: `自动段落 ${chapterNum}`,
start_line: startLine,
content: currentContent.trim()
});
currentContent = '';
startLine = i + 2;
consecutiveEmpty = 0;
}
}
// 最后一段
if (currentContent.trim().length > 0) {
chapterNum++;
chapters.push({
title: chapters.length === 0 ? '全文' : `自动段落 ${chapterNum}`,
start_line: startLine,
content: currentContent.trim()
});
}
return chapters;
}
/**
* 检测使用了哪种分割方法
*/
function detectMethod(chapters) {
if (chapters.length === 0) return 'none';
const firstTitle = chapters[0].title;
if (CHAPTER_PATTERNS.some(p => p.test(firstTitle))) return 'pattern';
if (firstTitle.startsWith('段落')) return 'separator';
if (firstTitle.startsWith('自动段落')) return 'paragraph';
return 'mixed';
}
/**
* 获取已分章的书籍列表
*/
function listChapteredBooks() {
if (!fs.existsSync(CHAPTERS_DIR)) return [];
return fs.readdirSync(CHAPTERS_DIR)
.filter(d => {
const indexPath = path.join(CHAPTERS_DIR, d, 'index.json');
return fs.existsSync(indexPath);
})
.map(d => {
const indexPath = path.join(CHAPTERS_DIR, d, 'index.json');
try {
return JSON.parse(fs.readFileSync(indexPath, 'utf8'));
} catch {
return null;
}
})
.filter(Boolean);
}
/**
* 获取某本书的分章详情
*/
function getBookChapters(bookId) {
if (!sanitizeId(bookId)) return null;
const indexPath = path.join(CHAPTERS_DIR, bookId, 'index.json');
const resolvedPath = path.resolve(indexPath);
const resolvedBase = path.resolve(CHAPTERS_DIR);
if (!resolvedPath.startsWith(resolvedBase)) return null;
if (!fs.existsSync(indexPath)) return null;
try {
return JSON.parse(fs.readFileSync(indexPath, 'utf8'));
} catch {
return null;
}
}
/**
* 获取某章内容
*/
function getChapterContent(bookId, chapterIndex) {
if (!sanitizeId(bookId)) return null;
const chapterPath = path.join(
CHAPTERS_DIR,
bookId,
`chapter-${String(chapterIndex).padStart(4, '0')}.txt`
);
// 防止路径遍历
const resolvedPath = path.resolve(chapterPath);
const resolvedBase = path.resolve(CHAPTERS_DIR);
if (!resolvedPath.startsWith(resolvedBase)) return null;
if (!fs.existsSync(chapterPath)) return null;
return fs.readFileSync(chapterPath, 'utf8');
}
/**
* 获取分章引擎统计
*/
function getStats() {
const books = listChapteredBooks();
return {
total_chaptered_books: books.length,
total_chapters: books.reduce((sum, b) => sum + (b.total_chapters || 0), 0),
total_chars: books.reduce((sum, b) => sum + (b.total_chars || 0), 0),
methods: {
pattern: books.filter(b => b.split_method === 'pattern').length,
separator: books.filter(b => b.split_method === 'separator').length,
paragraph: books.filter(b => b.split_method === 'paragraph').length
}
};
}
// 初始化
init();
module.exports = {
splitChapters,
listChapteredBooks,
getBookChapters,
getChapterContent,
getStats,
CHAPTER_PATTERNS
};