返回 oh-my-ppt
finalization.ts
根目录 / src / main / generation / finalization.ts
1 import log from 'electron-log/main.js'
2 import path from 'path'
3 import { customAlphabet, nanoid } from 'nanoid'
4 import type { GenerationContext } from './context'
5 import type { FinalizeContext, FinalizeGenerationArgs } from './types'
6 import type { SessionPageRecord } from '../db/database'
7 import { isCancellationMessage, normalizeRestoredSessionStatus } from './status-utils'
8
9 const pageSlugId = customAlphabet('abcdefghijklmnopqrstuvwxyz0123456789', 10)
10
11 const assertNotCancelled = (context: FinalizeContext): void => {
12 if (context.abortSignal?.aborted) throw new Error('生成已取消')
13 }
14
15 export const resolveGenerationFailureSessionStatus = (
16 context: FinalizeContext,
17 cancelled: boolean
18 ): 'active' | 'completed' | 'failed' | 'archived' => {
19 if (cancelled) return normalizeRestoredSessionStatus(context.previousSessionStatus)
20 if (
21 (context.effectiveMode === 'edit' ||
22 context.effectiveMode === 'retry' ||
23 context.effectiveMode === 'addPage' ||
24 context.effectiveMode === 'retrySinglePage') &&
25 context.previousSessionStatus !== 'active'
26 ) {
27 return normalizeRestoredSessionStatus(context.previousSessionStatus)
28 }
29 return 'failed'
30 }
31
32 const syncGeneratedPagesToSessionPages = async (
33 ctx: GenerationContext,
34 args: {
35 sessionId: string
36 runId: string
37 generatedPages: Array<{
38 id?: string
39 pageNumber: number
40 title: string
41 pageId?: string
42 htmlPath?: string
43 layoutIntent?: string | null
44 layoutId?: string | null
45 layoutContractVersion?: number | null
46 }>
47 }
48 ): Promise<void> => {
49 const existingPages = await ctx.db.listSessionPages(args.sessionId, { includeDeleted: true })
50 const generationPages = await ctx.db.listGenerationPages(args.runId)
51 const generationPageById = new Map(generationPages.map((page) => [page.page_id, page]))
52 const existingBySlug = new Map<string, SessionPageRecord>()
53 for (const row of existingPages) {
54 existingBySlug.set(row.file_slug, row)
55 if (row.legacy_page_id) existingBySlug.set(row.legacy_page_id, row)
56 }
57
58 for (const page of args.generatedPages) {
59 const fileSlug = page.pageId || `page-${pageSlugId()}`
60 const existing = existingBySlug.get(fileSlug)
61 const generationPage = generationPageById.get(fileSlug)
62 await ctx.db.upsertSessionPage({
63 id: page.id || existing?.id || nanoid(),
64 sessionId: args.sessionId,
65 legacyPageId: existing?.legacy_page_id || (fileSlug.match(/^page-\d+$/) ? fileSlug : null),
66 fileSlug,
67 pageNumber: page.pageNumber,
68 title: page.title || `第 ${page.pageNumber} 页`,
69 htmlPath: page.htmlPath || '',
70 layoutIntent:
71 page.layoutIntent ?? generationPage?.layout_intent ?? existing?.layout_intent ?? null,
72 layoutId: page.layoutId ?? generationPage?.layout_id ?? existing?.layout_id ?? null,
73 layoutContractVersion:
74 page.layoutContractVersion ??
75 generationPage?.layout_contract_version ??
76 existing?.layout_contract_version ??
77 null,
78 status: 'completed',
79 error: null
80 })
81 }
82 }
83
84 export async function finalizeGenerationSuccess(
85 ctx: GenerationContext,
86 args: FinalizeGenerationArgs
87 ): Promise<void> {
88 const { db } = ctx
89 const { context, indexPath, totalPages, generatedPages } = args
90 const contextWithPrompt = context as FinalizeContext & { userMessage?: unknown }
91 assertNotCancelled(context)
92 await syncGeneratedPagesToSessionPages(ctx, {
93 sessionId: context.sessionId,
94 runId: context.runId,
95 generatedPages
96 })
97 assertNotCancelled(context)
98 await db.updateSessionMetadata(context.sessionId, {
99 lastRunId: context.runId,
100 entryMode: 'multi_page',
101 indexPath,
102 projectId: context.projectId
103 })
104 assertNotCancelled(context)
105 if (args.designContract) {
106 await db.updateSessionDesignContract(context.sessionId, args.designContract)
107 }
108 assertNotCancelled(context)
109 await db.updateProjectStatus(context.projectId, 'draft')
110 assertNotCancelled(context)
111 await db.updateSessionStatus(context.sessionId, 'completed')
112 assertNotCancelled(context)
113 await ctx.history.recordOperation({
114 sessionId: context.sessionId,
115 projectDir: path.dirname(indexPath),
116 type:
117 context.effectiveMode === 'addPage'
118 ? 'addPage'
119 : context.effectiveMode === 'retry'
120 ? 'retry'
121 : context.effectiveMode === 'retrySinglePage'
122 ? 'retry'
123 : 'generate',
124 scope: context.effectiveMode === 'retrySinglePage' ? 'page' : 'session',
125 prompt: typeof contextWithPrompt.userMessage === 'string' ? contextWithPrompt.userMessage : null,
126 metadata: {
127 runId: context.runId,
128 effectiveMode: context.effectiveMode,
129 totalPages
130 }
131 })
132 assertNotCancelled(context)
133 await db.updateGenerationRunStatus(context.runId, 'completed', null)
134 assertNotCancelled(context)
135 log.info('[generate:start] completed', {
136 sessionId: context.sessionId,
137 styleId: context.styleId,
138 totalPages
139 })
140 ctx.runtimeEmitters.emitGenerateChunk(context.sessionId, {
141 type: 'run_completed',
142 payload: {
143 runId: context.runId,
144 totalPages
145 }
146 })
147 }
148
149 export async function finalizeGenerationFailure(
150 ctx: GenerationContext,
151 context: FinalizeContext,
152 error: unknown
153 ): Promise<void> {
154 const { db } = ctx
155 const message =
156 error instanceof Error && error.message.length > 0 ? error.message : 'Generation failed'
157 const cancelled = isCancellationMessage(message)
158 log.error('[generate:start] failed', {
159 sessionId: context.sessionId,
160 styleId: context.styleId,
161 message
162 })
163 const generationRun = await db.getGenerationRun(context.runId)
164 if (generationRun && (generationRun.status === 'running' || generationRun.status === 'completed')) {
165 await db.updateGenerationRunStatus(context.runId, 'failed', message)
166 }
167 if (context.effectiveMode === 'addPage' && context.targetPageId) {
168 const targetPage = (await db.listSessionPages(context.sessionId)).find(
169 (page) => page.id === context.targetPageId || page.file_slug === context.targetPageId
170 )
171 if (targetPage) {
172 await db.upsertSessionPage({
173 id: targetPage.id,
174 sessionId: targetPage.session_id,
175 legacyPageId: targetPage.legacy_page_id,
176 fileSlug: targetPage.file_slug,
177 pageNumber: targetPage.page_number,
178 title: targetPage.title,
179 htmlPath: targetPage.html_path,
180 status: 'failed',
181 error: message
182 })
183 }
184 }
185 await db.updateSessionStatus(
186 context.sessionId,
187 resolveGenerationFailureSessionStatus(context, cancelled)
188 )
189 await db.addMessage(context.sessionId, {
190 role: 'system',
191 content: message,
192 type: 'stream_chunk',
193 chat_scope: context.messageScope,
194 page_id: context.messagePageId,
195 run_model: context.runModel
196 })
197 ctx.runtimeEmitters.emitGenerateChunk(context.sessionId, {
198 type: 'run_error',
199 payload: { runId: context.runId, message, cancelled }
200 })
201 }
202
202 lines TYPESCRIPT