返回 oh-my-ppt
stage-manager.ts
根目录 / src / main / thinking / stage-manager.ts
1 import type { ThinkingStage } from '@shared/thinking'
2 import { routeThinkingIntent } from './intent-router'
3
4 export const VALID_TRANSITIONS: Record<ThinkingStage, ThinkingStage[]> = {
5 collect: ['collect', 'outline'],
6 outline: ['collect', 'draft', 'ready'],
7 draft: ['collect', 'outline', 'refine', 'ready'],
8 refine: ['collect', 'outline', 'draft', 'ready'],
9 ready: ['collect', 'outline']
10 }
11
12 export function checkStageTransition(
13 currentStage: ThinkingStage,
14 thinkingMd: string
15 ): ThinkingStage {
16 const pageCount = countPageHeadings(thinkingMd)
17 const hasTopic = hasNonEmptySection(thinkingMd, 'Topic')
18
19 if (currentStage === 'collect' && hasTopic && pageCount >= 2) {
20 return 'outline'
21 }
22
23 return currentStage
24 }
25
26 export function detectStageFallback(userMessage: string): ThinkingStage | null {
27 return routeThinkingIntent({ userMessage }).requestedStage
28 }
29
30 export function resolveRequestedStage(args: {
31 currentStage: ThinkingStage
32 requestedStage: ThinkingStage | null
33 thinkingMd: string
34 }): ThinkingStage | null {
35 if (!args.requestedStage) return null
36 if (args.requestedStage === args.currentStage) return args.requestedStage
37 if (!VALID_TRANSITIONS[args.currentStage].includes(args.requestedStage)) return null
38 if (!hasThinkingContentForStage(args.requestedStage, args.thinkingMd)) return null
39
40 return args.requestedStage
41 }
42
43 export function isValidTransition(from: ThinkingStage, to: ThinkingStage): boolean {
44 return VALID_TRANSITIONS[from].includes(to)
45 }
46
47 function hasThinkingContentForStage(stage: ThinkingStage, thinkingMd: string): boolean {
48 if (stage === 'collect') return true
49
50 const pageCount = countPageHeadings(thinkingMd)
51 const hasTopic = hasNonEmptySection(thinkingMd, 'Topic')
52 if (stage === 'outline') return hasTopic && pageCount >= 2
53
54 return hasTopic && hasCompletePagePlan(thinkingMd)
55 }
56
57 export function isRestartRequest(userMessage: string): boolean {
58 return /let's start over|start over|从头开始|重新开始/i.test(userMessage)
59 }
60
61 function countPageHeadings(thinkingMd: string): number {
62 const matches = thinkingMd.match(/^##\s*Page\s+\d+\s*:/gm)
63 return matches ? matches.length : 0
64 }
65
66 function hasCompletePagePlan(thinkingMd: string): boolean {
67 const pageSections = getPageSections(thinkingMd)
68 return pageSections.length >= 2 && pageSections.every(hasCompletePageSection)
69 }
70
71 function getPageSections(thinkingMd: string): string[] {
72 const headingRegex = /^##\s*Page\s+\d+\s*:/gm
73 const headings = Array.from(thinkingMd.matchAll(headingRegex))
74 return headings.map((heading, index) => {
75 const start = heading.index || 0
76 const next = headings[index + 1]
77 const end = typeof next?.index === 'number' ? next.index : thinkingMd.length
78 return thinkingMd.slice(start, end).trim()
79 })
80 }
81
82 function hasCompletePageSection(pageSection: string): boolean {
83 const hasTitle = /^##\s*Page\s+\d+\s*:\s*\S+/m.test(pageSection)
84 const hasRole = /^-\s*Role:\s*\S+/mi.test(pageSection)
85 const hasObjective = /^-\s*Objective:\s*\S+/mi.test(pageSection)
86 const contentLines = pageSection
87 .split('\n')
88 .map((line) => line.trim())
89 .filter(Boolean)
90 .filter((line) => !/^##\s*Page\s+\d+\s*:/i.test(line))
91 .filter((line) => !/^-\s*(Role|Objective):/i.test(line))
92 const hasSummary = contentLines.some((line) => !line.startsWith('- '))
93 const hasKeyPoints = contentLines.some((line) => /^-\s+\S+/.test(line))
94
95 return hasTitle && hasRole && hasObjective && hasSummary && hasKeyPoints
96 }
97
98 function hasNonEmptySection(markdown: string, heading: string): boolean {
99 const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
100 const inline = markdown.match(new RegExp(`^##\\s*${escaped}\\s*:\\s*(.+)`, 'm'))
101 if (inline?.[1]?.trim()) return true
102 const block = markdown.match(new RegExp(`^##\\s*${escaped}\\s*\\n([\\s\\S]*?)(?=^##\\s+|(?![\\s\\S]))`, 'm'))
103 return Boolean(block?.[1]?.trim())
104 }
105
105 lines TYPESCRIPT