| 1 | import path from 'path' |
| 2 | import type { SourceDocumentPlan } from '@shared/generation' |
| 3 | |
| 4 | const MAX_THINKING_PAGE_OUTLINE_CHARS = 520 |
| 5 | const MAX_THINKING_PAGE_KEY_POINTS = 10 |
| 6 | |
| 7 | const compactThinkingOutlineText = (value: string): string => value.replace(/\s+/g, ' ').trim() |
| 8 | |
| 9 | const readThinkingPageField = (blockLines: string[], field: string): string => { |
| 10 | const pattern = new RegExp(`^-\\s*${field}\\s*:`, 'i') |
| 11 | return ( |
| 12 | blockLines |
| 13 | .find((line) => pattern.test(line.trim())) |
| 14 | ?.replace(pattern, '') |
| 15 | .trim() || '' |
| 16 | ) |
| 17 | } |
| 18 | |
| 19 | export const buildThinkingPageOutline = (blockLines: string[]): string => { |
| 20 | const objective = readThinkingPageField(blockLines, 'Objective') |
| 21 | const summaryLines: string[] = [] |
| 22 | const keyPointLines: string[] = [] |
| 23 | let collectingSummary = false |
| 24 | |
| 25 | for (const rawLine of blockLines) { |
| 26 | const line = rawLine.trim() |
| 27 | if (!line) { |
| 28 | collectingSummary = false |
| 29 | continue |
| 30 | } |
| 31 | if (/^-\s*(Role|Objective)\s*:/i.test(line)) continue |
| 32 | if (/^-\s+/.test(line)) { |
| 33 | keyPointLines.push(line.replace(/^-\s+/, '').trim()) |
| 34 | collectingSummary = false |
| 35 | continue |
| 36 | } |
| 37 | if (!collectingSummary && summaryLines.length > 0) continue |
| 38 | summaryLines.push(line) |
| 39 | collectingSummary = true |
| 40 | } |
| 41 | |
| 42 | const parts = [ |
| 43 | objective, |
| 44 | compactThinkingOutlineText(summaryLines.join(' ')), |
| 45 | ...keyPointLines.slice(0, MAX_THINKING_PAGE_KEY_POINTS).map(compactThinkingOutlineText) |
| 46 | ].filter(Boolean) |
| 47 | |
| 48 | return compactThinkingOutlineText(parts.join(';')).slice(0, MAX_THINKING_PAGE_OUTLINE_CHARS) |
| 49 | } |
| 50 | |
| 51 | export function buildThinkingSourcePlan( |
| 52 | thinkingMd: string, |
| 53 | thinkingDocumentPath: string |
| 54 | ): SourceDocumentPlan | null { |
| 55 | const lines = thinkingMd.split(/\r?\n/) |
| 56 | const headings: Array<{ pageNumber: number; title: string; lineNumber: number }> = [] |
| 57 | |
| 58 | lines.forEach((line, index) => { |
| 59 | const match = line.match(/^##\s*Page\s+(\d+)\s*:\s*(.+)$/) |
| 60 | if (!match) return |
| 61 | const pageNumber = Number.parseInt(match[1], 10) |
| 62 | const title = match[2].trim() |
| 63 | if (!Number.isFinite(pageNumber) || !title) return |
| 64 | headings.push({ pageNumber, title, lineNumber: index + 1 }) |
| 65 | }) |
| 66 | |
| 67 | if (headings.length === 0) return null |
| 68 | |
| 69 | return { |
| 70 | version: 1, |
| 71 | confidence: 'high', |
| 72 | sourceDocumentPath: thinkingDocumentPath, |
| 73 | sourceDocumentName: path.basename(thinkingDocumentPath), |
| 74 | pageSkeleton: headings.map((heading, index) => { |
| 75 | const next = headings[index + 1] |
| 76 | const lineStart = heading.lineNumber |
| 77 | const lineEnd = Math.max(lineStart, next ? next.lineNumber - 1 : lines.length) |
| 78 | const blockLines = lines.slice(heading.lineNumber, lineEnd) |
| 79 | const roleText = readThinkingPageField(blockLines, 'Role') |
| 80 | const pageOutline = buildThinkingPageOutline(blockLines) |
| 81 | const roleBasis = `${heading.title}\n${roleText}` |
| 82 | const role = /chapter|section|divider|cover|title|agenda|章节|过渡|封面|目录|标题/i.test( |
| 83 | roleBasis |
| 84 | ) |
| 85 | ? 'chapter-divider' |
| 86 | : 'content' |
| 87 | |
| 88 | return { |
| 89 | pageNumber: heading.pageNumber, |
| 90 | title: heading.title, |
| 91 | role, |
| 92 | sourceHeading: `Page ${heading.pageNumber}: ${heading.title}`, |
| 93 | headingLevel: 2, |
| 94 | lineStart, |
| 95 | lineEnd, |
| 96 | reason: pageOutline || roleText || 'Thinking page section' |
| 97 | } |
| 98 | }) |
| 99 | } |
| 100 | } |
| 101 |