ChapterWriter.js 2.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. import { promises as fs } from 'node:fs'
  2. import path from 'node:path'
  3. import { serializeFrontMatter } from '../serializers/front-matter.js'
  4. /**
  5. * ChapterWriter:写新章到定稿(M2 定稿流程调用)。
  6. */
  7. /** 文件名净化:Windows 非法字符 <>:"/\|?* 与控制字符替成 _(标题本体不改,只净化文件名)。 */
  8. function sanitizeFileName(title) {
  9. const s = String(title).replace(/[<>:"/\\|?*\x00-\x1f]/g, '_').replace(/\s+/g, ' ').trim()
  10. return s || '未命名'
  11. }
  12. /** 删除同章旧文件(标题可能不同),避免 scanChapters 撞 PRIMARY KEY(P0-3a)。 */
  13. async function removeOldChapterFiles(dir, chapterNum, safeTitle) {
  14. const prefix = `${String(chapterNum).padStart(4, '0')}-`
  15. const target = `${prefix}${safeTitle}.md`
  16. let files = []
  17. try {
  18. files = await fs.readdir(dir)
  19. } catch {
  20. return
  21. }
  22. for (const f of files) {
  23. if (!f.startsWith(prefix) || !f.endsWith('.md')) continue
  24. if (f === target) continue
  25. await fs.rm(path.join(dir, f), { force: true })
  26. }
  27. }
  28. export class ChapterWriter {
  29. constructor(repoPath, cache = null) {
  30. this.repoPath = repoPath
  31. this.cache = cache
  32. }
  33. /**
  34. * 写新章到 定稿/正文/NNNN-标题.md(front matter 走防呆序列化)。
  35. * @param {number} chapterNum
  36. * @param {object} frontMatter - 章档案(章号/标题/卷/视角/字数/章定位/钩子/情绪定位/伏笔[]/...)
  37. * @param {string} body - 正文(不含 front matter)
  38. * @returns {Promise<{ok: boolean, filePath: string, error: string}>}
  39. */
  40. async writeChapter(chapterNum, frontMatter, body) {
  41. try {
  42. const title = frontMatter.标题 || '未命名'
  43. const safeTitle = sanitizeFileName(title)
  44. const dir = path.join(this.repoPath, '定稿', '正文')
  45. await fs.mkdir(dir, { recursive: true })
  46. await removeOldChapterFiles(dir, chapterNum, safeTitle)
  47. const fileName = `${String(chapterNum).padStart(4, '0')}-${safeTitle}.md`
  48. const filePath = path.join(dir, fileName)
  49. await fs.writeFile(filePath, serializeFrontMatter(frontMatter, body), 'utf8')
  50. return { ok: true, filePath, error: '' }
  51. } catch (err) {
  52. return { ok: false, filePath: '', error: `写章节 ${chapterNum} 失败:${err.message}` }
  53. }
  54. }
  55. /**
  56. * 更新已有章节 front matter(M2 写新章流程不需要,按需补)。
  57. */
  58. async updateFrontMatter(chapterNum, updates) {
  59. throw new Error('ChapterWriter.updateFrontMatter() 暂未实现(M2 写新章不依赖;按需补)')
  60. }
  61. }