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