chapter-selection.ts 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. import type { ChatMessage } from "@/lib/llm-providers"
  2. import { parseFrontmatter } from "@/lib/frontmatter"
  3. export interface ChapterBodySelection {
  4. start: number
  5. end: number
  6. text: string
  7. bodySnapshot: string
  8. }
  9. export type ChapterSelectionAction = "polish" | "de-ai"
  10. const POLISH_SYSTEM_PROMPT = [
  11. "请在不改动核心剧情事实、人物关系、时间线和关键设定的前提下润色表达。",
  12. "保留原段落的叙事视角、信息密度和情绪走向。",
  13. "增强画面感、节奏感、动作细节和文字流畅度,但不要扩写成新的剧情。",
  14. "输出仅返回润色后的正文片段,不要解释。",
  15. ].join("\n")
  16. export function splitChapterHeading(markdown: string): { heading: string; body: string } {
  17. const match = markdown.match(/^#\s+(.+)$/m)
  18. if (!match) return { heading: "", body: markdown }
  19. const heading = match[1]
  20. const body = markdown.replace(/^#\s+.+$/m, "").replace(/^\n+/, "")
  21. return { heading, body }
  22. }
  23. export function rebuildChapterBody(heading: string, body: string): string {
  24. return heading ? `# ${heading}\n\n${body}` : body
  25. }
  26. export function replaceWholeChapterBody(currentMarkdown: string, replacementMarkdown: string): string {
  27. const { rawBlock, body: currentBody } = parseFrontmatter(currentMarkdown)
  28. const { body: replacementBodyRaw } = parseFrontmatter(replacementMarkdown)
  29. const { heading } = splitChapterHeading(currentBody)
  30. const { body: replacementBody } = splitChapterHeading(replacementBodyRaw)
  31. return rawBlock + rebuildChapterBody(heading, replacementBody)
  32. }
  33. export function buildPolishSelectionMessages(content: string): ChatMessage[] {
  34. if (!content.trim()) throw new Error("润色内容为空,无法处理")
  35. return [
  36. { role: "system", content: POLISH_SYSTEM_PROMPT },
  37. {
  38. role: "user",
  39. content: "请润色下面选中的小说正文片段。\n\n输出仅返回润色后的正文片段,不要解释。\n\n正文如下:\n\n" + content,
  40. },
  41. ]
  42. }
  43. export function replaceChapterBodySelection(
  44. currentBody: string,
  45. selection: ChapterBodySelection,
  46. replacement: string,
  47. ): { ok: true; body: string } | { ok: false; reason: "changed" | "empty" } {
  48. if (!selection.text.trim()) {
  49. return { ok: false, reason: "empty" }
  50. }
  51. const { start, end, bodySnapshot, text } = selection
  52. if (bodySnapshot !== currentBody) {
  53. return { ok: false, reason: "changed" }
  54. }
  55. if (currentBody.slice(start, end) !== text) {
  56. return { ok: false, reason: "changed" }
  57. }
  58. return {
  59. ok: true,
  60. body: `${currentBody.slice(0, start)}${replacement}${currentBody.slice(end)}`,
  61. }
  62. }