返回 oh-my-ppt
edit-deck-allpage-flow.ts
根目录 / src / main / generation / edit-deck-allpage-flow.ts
1 import fs from 'fs'
2 import path from 'path'
3 import log from 'electron-log/main.js'
4 import { nanoid } from 'nanoid'
5 import { progressText } from '@shared/progress'
6 import { normalizeLayoutIntent } from '@shared/layout-intent'
7 import {
8 MAX_SELECTED_PAGES,
9 MAX_STYLE_SWITCH_PAGES,
10 type DesignContract,
11 type GeneratedPagePayload,
12 type PageReferenceContext
13 } from '@shared/generation'
14 import type { GenerationContext } from './context'
15 import type { EditContext, EmitAssistantFn } from './types'
16 import {
17 buildEditNoChangeRetryMessage,
18 buildEditToolSchemaRetryMessage,
19 buildEditValidationRetryMessage,
20 isEditToolSchemaRetryableError,
21 isEditValidationRetryableError,
22 resolvePageHtmlPath,
23 uiText,
24 validateChangedPages
25 } from './generation-utils'
26 import {
27 executeDeckEditBatchFlow,
28 type DeckEditBatchResult,
29 type DeckEditCompletedBatch,
30 type DeckEditFailedBatch
31 } from './edit-deck-batch-flow'
32 import { runDeepAgentDeckAllPageEdit } from './agent-runner'
33 import { createPageImageFinalizer } from './page-image-finalizer'
34 import { hasCompatiblePageLayoutSource, validateLayoutSlots } from './layout-slot-validator'
35 import { buildGenerationImageIntentRules } from '../agent-runtime/prompt/composers/generation-image-intent-rules'
36 import { getLayoutMasterTemplate } from '@shared/layout-master'
37 import { resolveRemainingFailedPageInfo } from './edit-deck-failure-state'
38 import { resolvePageReferenceContext } from './source-plan'
39 import {
40 buildLocalSuccessfulEditSummary,
41 emitSuccessfulEditSummary
42 } from './edit-summary'
43
44 export function filterPageRefsBySelectedPageIds<T extends { pageId: string }>(
45 pageRefs: T[],
46 selectPageIds: string[]
47 ): T[] {
48 if (selectPageIds.length === 0) return pageRefs
49 const requestedPageIdSet = new Set(selectPageIds)
50 return pageRefs.filter((ref) => requestedPageIdSet.has(ref.pageId))
51 }
52
53 export const isDeckEditRateLimitRetryableError = (error: unknown): boolean => {
54 const message = error instanceof Error ? error.message : String(error || '')
55 return /\b429\b|too many requests|rate.?limit|resource exhausted/i.test(message)
56 }
57
58 export async function executeDeckAllPageEditGeneration(
59 ctx: GenerationContext,
60 emitAssistant: EmitAssistantFn,
61 context: EditContext
62 ): Promise<void> {
63 const {
64 db,
65 agentManager,
66 sessionProject: { getPageSourceUrl },
67 runtimeEmitters: { createDeckProgressEmitter },
68 tuning: { pageEditDefaultTemperature: PAGE_EDIT_DEFAULT_TEMPERATURE }
69 } = ctx
70
71 if (!context.apiKey) {
72 throw new Error(`当前 provider "${context.provider}" 缺少 API Key,请先到设置页配置。`)
73 }
74 if (context.messageScope !== 'main') {
75 throw new Error('deck 全页编辑只接受主会话消息。')
76 }
77
78 const projectDir = context.projectDir
79 const indexPath = path.join(projectDir, 'index.html')
80 let outlineTitles: string[] = context.userProvidedOutlineTitles
81 let pageRefs: Array<{
82 id: string
83 pageNumber: number
84 title: string
85 pageId: string
86 htmlPath: string
87 }> = []
88 let savedDesignContract: DesignContract | undefined = context.designContract
89
90 const sessionPages = await db.listSessionPages(context.sessionId)
91 if (sessionPages.length === 0) {
92 throw new Error('session_pages is empty after migration; cannot edit this session')
93 }
94 const layoutSourceByPageId = new Map(
95 sessionPages.map((page) => [
96 page.file_slug,
97 {
98 layoutIntent: page.layout_intent ? normalizeLayoutIntent(page.layout_intent) : null,
99 layoutId: page.layout_id,
100 layoutContractVersion: page.layout_contract_version
101 }
102 ])
103 )
104 const pageReferenceContexts: Record<string, PageReferenceContext> = {}
105 for (const page of sessionPages) {
106 const pageReferenceContext = resolvePageReferenceContext({
107 referenceDocumentPath: context.referenceDocumentPath,
108 sourcePlan: context.sourcePlan,
109 pageNumber: page.page_number
110 })
111 if (pageReferenceContext) pageReferenceContexts[page.file_slug] = pageReferenceContext
112 }
113 pageRefs = sessionPages.map((page) => ({
114 id: page.id,
115 pageNumber: page.page_number,
116 title: page.title || `第${page.page_number}页`,
117 pageId: page.file_slug,
118 htmlPath: resolvePageHtmlPath({
119 projectDir,
120 fileSlug: page.file_slug,
121 candidates: [page.html_path]
122 })
123 }))
124 if (outlineTitles.length === 0) {
125 outlineTitles = pageRefs.map((page) => page.title)
126 }
127
128 const latestPageSnapshot = await db.listLatestGenerationPageSnapshot(context.sessionId)
129 const failedPageInfoById = new Map<string, { title: string; reason: string }>()
130 for (const page of sessionPages) {
131 if (page.status !== 'failed') continue
132 failedPageInfoById.set(page.file_slug, {
133 title: page.title || page.file_slug,
134 reason: page.error || '页面仍需修复'
135 })
136 }
137
138 const sessionRecord = (context.session || {}) as Record<string, unknown>
139 if (
140 !savedDesignContract &&
141 !context.resetVisualStyle &&
142 typeof sessionRecord.designContract === 'string' &&
143 sessionRecord.designContract.trim().length > 0
144 ) {
145 try {
146 savedDesignContract = JSON.parse(sessionRecord.designContract) as DesignContract
147 } catch {
148 /* ignore invalid persisted design contract */
149 }
150 }
151
152 pageRefs.sort((a, b) => a.pageNumber - b.pageNumber)
153 const requestedPageIdSet = new Set(context.selectPageIds || [])
154 const selectedPageRefs = filterPageRefsBySelectedPageIds(pageRefs, context.selectPageIds || [])
155 if (requestedPageIdSet.size > 0 && selectedPageRefs.length === 0) {
156 throw new Error(
157 `Selected pages not found in session_pages: ${Array.from(requestedPageIdSet).join(', ')}`
158 )
159 }
160 const pageLimit = context.resetVisualStyle ? MAX_STYLE_SWITCH_PAGES : MAX_SELECTED_PAGES
161 if (selectedPageRefs.length > pageLimit) {
162 throw new Error(
163 uiText(
164 context.appLocale,
165 `一次最多编辑 ${pageLimit} 页,请先选择更小的页面范围。`,
166 `You can edit at most ${pageLimit} pages at a time. Select a smaller page range.`
167 )
168 )
169 }
170 if (outlineTitles.length !== pageRefs.length) {
171 outlineTitles = pageRefs.map((ref) => ref.title)
172 }
173
174 const outlineByPageId = new Map(
175 latestPageSnapshot.map((page) => [page.page_id, page.content_outline || ''])
176 )
177 const layoutIntentByPageId = new Map(
178 latestPageSnapshot.map((page) => [
179 page.page_id,
180 !context.resetVisualStyle && page.layout_intent
181 ? normalizeLayoutIntent(page.layout_intent)
182 : undefined
183 ])
184 )
185 const outlineItems = pageRefs.map((ref) => ({
186 title: ref.title,
187 contentOutline: outlineByPageId.get(ref.pageId) || '',
188 layoutIntent: layoutSourceByPageId.get(ref.pageId)?.layoutIntent || layoutIntentByPageId.get(ref.pageId),
189 layoutId: layoutSourceByPageId.get(ref.pageId)?.layoutId || undefined
190 }))
191 const pageFileMap = Object.fromEntries(pageRefs.map((p) => [p.pageId, p.htmlPath]))
192 const pageNumbers = Object.fromEntries(pageRefs.map((p) => [p.pageId, p.pageNumber]))
193 const selectedPageIds = selectedPageRefs.map((p) => p.pageId)
194 const existingPageIdsBeforeRun: string[] = []
195 const beforeReads = await Promise.all(
196 pageRefs.map(async (ref) => {
197 if (!fs.existsSync(ref.htmlPath)) return null
198 const html = await fs.promises.readFile(ref.htmlPath, 'utf-8')
199 return { pageId: ref.pageId, html }
200 })
201 )
202 for (const item of beforeReads) {
203 if (!item) continue
204 existingPageIdsBeforeRun.push(item.pageId)
205 }
206
207 if (!context.skipGenerationRunCreation) {
208 await db.createGenerationRun({
209 id: context.runId,
210 sessionId: context.sessionId,
211 mode: 'edit',
212 totalPages: selectedPageRefs.length,
213 modelConfigId: context.modelConfigId,
214 metadata: {
215 editScope: 'deck',
216 selectedPageId: null,
217 selectPageIds: selectedPageIds,
218 selector: null,
219 modelConfigId: context.modelConfigId,
220 modelConfigName: context.modelConfigName,
221 provider: context.provider,
222 model: context.model
223 }
224 })
225 }
226
227 const emitEditChunk = createDeckProgressEmitter(context.sessionId, context.appLocale)
228 emitEditChunk({
229 type: 'stage_started',
230 payload: {
231 runId: context.runId,
232 stage: 'editing',
233 label: uiText(context.appLocale, '正在准备批量编辑', 'Preparing batch edit'),
234 progress: 10,
235 totalPages: selectedPageRefs.length
236 }
237 })
238
239 await ctx.history.ensureBaseline(context.sessionId, projectDir)
240
241 const editRunArgs = {
242 sessionId: context.sessionId,
243 provider: context.provider,
244 apiKey: context.apiKey,
245 model: context.model,
246 baseUrl: context.providerBaseUrl,
247 maxTokens: context.maxTokens,
248 modelTimeoutMs: context.modelTimeouts.agent,
249 temperature: PAGE_EDIT_DEFAULT_TEMPERATURE,
250 styleId: context.styleId,
251 styleSkillPrompt: context.styleSkill.prompt,
252 hasStyleImageDirection: Boolean(context.imageGenerationPrompt.trim()),
253 styleKey: context.styleKey,
254 styleName: context.styleName,
255 styleVersion: context.styleVersion,
256 slideSize: context.slideSize,
257 appLocale: context.appLocale,
258 topic: context.topic,
259 deckTitle: context.deckTitle,
260 userMessage: context.userMessage,
261 imageIntentAddendum:
262 context.visualEnabled && context.imageGenerationPrompt.trim()
263 ? [
264 'Automatic image generation is enabled for this session.',
265 'The active style supports automatic image generation.',
266 'For the current page only, preserve its data-ppt-slot contract and follow the image-intent rules supplied by its selected layout master.'
267 ].join('\n')
268 : context.visualEnabled
269 ? 'The active style has no image-generation direction. Do not request generated images.'
270 : '',
271 outlineTitles,
272 outlineItems,
273 sourceDocumentPaths: context.sourceDocumentPaths,
274 referenceDocumentPath: context.referenceDocumentPath,
275 pageReferenceContexts,
276 projectDir,
277 indexPath,
278 pageFileMap,
279 pageNumbers,
280 designContract: savedDesignContract,
281 existingPageIds: existingPageIdsBeforeRun,
282 finalizeEditedPage: async (pageId, refineImageLayout) => {
283 const page = pageRefs.find((item) => item.pageId === pageId)
284 const source = layoutSourceByPageId.get(pageId)
285 if (!page) return
286 if (
287 !source?.layoutIntent ||
288 !source.layoutId ||
289 !source.layoutContractVersion ||
290 !hasCompatiblePageLayoutSource(source)
291 ) {
292 log.info('[images:fulfillment] page skipped', {
293 sessionId: context.sessionId,
294 runId: context.runId,
295 pageId,
296 reason: 'deck edit has no compatible layout source',
297 layoutIntent: source?.layoutIntent || null,
298 layoutId: source?.layoutId || null,
299 layoutContractVersion: source?.layoutContractVersion || null
300 })
301 return
302 }
303 const finalizer = createPageImageFinalizer(ctx, {
304 sessionId: context.sessionId,
305 runId: context.runId,
306 visualEnabled: context.visualEnabled,
307 imageModelConfigId: context.imageModelConfigId,
308 imageGenerationPrompt: context.imageGenerationPrompt,
309 imagePromptDirector: {
310 provider: context.provider,
311 apiKey: context.apiKey,
312 model: context.model,
313 baseUrl: context.providerBaseUrl,
314 maxTokens: context.maxTokens,
315 modelRuntime: context.modelRuntime,
316 modelTimeoutMs: context.modelTimeouts.agent,
317 locale: context.appLocale
318 },
319 abortSignal: context.abortSignal
320 })
321 await finalizer(
322 {
323 pageNumber: page.pageNumber,
324 pageId,
325 title: page.title,
326 contentOutline: outlineByPageId.get(pageId) || '',
327 layoutIntent: source.layoutIntent || undefined,
328 layoutId: source.layoutId,
329 layoutContractVersion: source.layoutContractVersion,
330 htmlPath: page.htmlPath
331 },
332 refineImageLayout
333 )
334 },
335 agentManager,
336 runId: context.runId,
337 signal: context.abortSignal
338 } satisfies Omit<Parameters<typeof runDeepAgentDeckAllPageEdit>[0], 'selectPageIds' | 'emit'>
339
340 const outlineItemByPageId = new Map(
341 pageRefs.map((page, index) => [page.pageId, outlineItems[index]])
342 )
343 const existingSessionPages = await db.listSessionPages(context.sessionId, {
344 includeDeleted: true
345 })
346 const existingBySlug = new Map(existingSessionPages.map((sp) => [sp.file_slug, sp]))
347 let batchResults: DeckEditBatchResult[]
348 try {
349 context.onDeckEditStarted?.()
350 batchResults = await executeDeckEditBatchFlow({
351 pageRefs: selectedPageRefs,
352 indexPath,
353 originalUserMessage: context.userMessage,
354 runId: context.runId,
355 appLocale: context.appLocale,
356 signal: context.abortSignal,
357 emit: emitEditChunk,
358 validateChangedPages,
359 buildRetryMessage: ({ baseMessage, error, kind }) => {
360 const detail = error instanceof Error ? error.message : String(error || '')
361 if (kind === 'no_change') {
362 return buildEditNoChangeRetryMessage({
363 originalMessage: baseMessage,
364 allowedTool: 'update_page_file',
365 selectedPageId: null
366 })
367 }
368 if (kind === 'validation' || isEditValidationRetryableError(error)) {
369 return buildEditValidationRetryMessage(baseMessage, detail)
370 }
371 if (isEditToolSchemaRetryableError(error)) {
372 return buildEditToolSchemaRetryMessage({
373 originalMessage: baseMessage,
374 detail,
375 allowedTool: 'update_page_file',
376 selectedPageId: null
377 })
378 }
379 if (isDeckEditRateLimitRetryableError(error)) {
380 return [
381 baseMessage,
382 '',
383 'Retry requirement:',
384 `- The previous page request was rate limited: ${detail}`,
385 '- Retry this page once after the configured stagger delay.',
386 '- Edit only the current page and do not modify index.html.'
387 ].join('\n')
388 }
389 return null
390 },
391 runPageAttempt: async ({ pageId, userMessage, isRetry, emit }) => {
392 if (isRetry) {
393 const retryPage = selectedPageRefs.find((page) => page.pageId === pageId)
394 const currentPage = Math.max(
395 1,
396 selectedPageRefs.findIndex((page) => page.pageId === pageId) + 1
397 )
398 emit({
399 type: 'llm_status',
400 payload: {
401 runId: context.runId,
402 stage: 'editing',
403 label: uiText(
404 context.appLocale,
405 `正在重试 P${retryPage?.pageNumber ?? currentPage}`,
406 `Retrying P${retryPage?.pageNumber ?? currentPage}`
407 ),
408 progress: 0,
409 currentPage,
410 totalPages: selectedPageRefs.length,
411 detail: uiText(
412 context.appLocale,
413 `正在重试页面:${pageId}`,
414 `Retrying page: ${pageId}`
415 )
416 }
417 })
418 }
419 return runDeepAgentDeckAllPageEdit({
420 ...editRunArgs,
421 userMessage,
422 imageIntentAddendum: (() => {
423 if (!context.visualEnabled) return ''
424 const source = layoutSourceByPageId.get(pageId)
425 if (
426 !source?.layoutIntent ||
427 !source.layoutId ||
428 !source.layoutContractVersion ||
429 !hasCompatiblePageLayoutSource(source)
430 ) {
431 return ''
432 }
433 const template = getLayoutMasterTemplate(source.layoutId)
434 return template
435 ? buildGenerationImageIntentRules({
436 visualEnabled: true,
437 template,
438 hasStyleImageDirection: Boolean(context.imageGenerationPrompt.trim())
439 })
440 : ''
441 })(),
442 selectPageIds: [pageId],
443 emit
444 })
445 },
446 onPageCompleted: async (result) => {
447 const pageRef = selectedPageRefs.find((p) => p.pageId === result.pageId)
448 if (!pageRef) return
449 const outlineItem = outlineItemByPageId.get(result.pageId)
450 const source = layoutSourceByPageId.get(result.pageId)
451 const currentHtml = await fs.promises.readFile(pageRef.htmlPath, 'utf-8')
452 const retainsLayoutSource = Boolean(
453 source?.layoutId &&
454 source.layoutContractVersion &&
455 validateLayoutSlots({
456 html: currentHtml,
457 layoutIntent: source.layoutIntent,
458 layoutId: source.layoutId,
459 layoutContractVersion: source.layoutContractVersion
460 }).valid
461 )
462 await db.upsertGenerationPage({
463 runId: context.runId,
464 sessionId: context.sessionId,
465 pageId: result.pageId,
466 pageNumber: pageRef.pageNumber,
467 title: pageRef.title,
468 contentOutline: outlineItem?.contentOutline || '',
469 layoutIntent: retainsLayoutSource
470 ? source?.layoutIntent || outlineItem?.layoutIntent
471 : null,
472 layoutId: retainsLayoutSource ? source?.layoutId : null,
473 layoutContractVersion: retainsLayoutSource ? source?.layoutContractVersion : null,
474 htmlPath: pageRef.htmlPath,
475 status: 'completed',
476 retryCount: result.retryCount
477 })
478 const existing = existingBySlug.get(result.pageId)
479 await db.upsertSessionPage({
480 id: existing?.id || nanoid(),
481 sessionId: context.sessionId,
482 legacyPageId:
483 existing?.legacy_page_id || (result.pageId.match(/^page-\d+$/) ? result.pageId : null),
484 fileSlug: result.pageId,
485 pageNumber: pageRef.pageNumber,
486 title: pageRef.title,
487 htmlPath: pageRef.htmlPath,
488 layoutIntent: retainsLayoutSource ? source?.layoutIntent : null,
489 layoutId: retainsLayoutSource ? source?.layoutId : null,
490 layoutContractVersion: retainsLayoutSource ? source?.layoutContractVersion : null,
491 status: 'completed',
492 error: null
493 })
494 for (const page of result.changedPages) {
495 const isExisting = existingPageIdsBeforeRun.includes(page.pageId)
496 const payload: GeneratedPagePayload = {
497 id: page.id,
498 focusPage: false,
499 pageNumber: page.pageNumber,
500 title: page.title,
501 html: page.html,
502 pageId: page.pageId,
503 htmlPath: page.htmlPath,
504 sourceUrl: getPageSourceUrl(page.htmlPath)
505 }
506 emitEditChunk({
507 type: isExisting ? 'page_updated' : 'page_generated',
508 payload: {
509 runId: context.runId,
510 stage: 'editing',
511 label: uiText(
512 context.appLocale,
513 `P${page.pageNumber} 修改结果已保存`,
514 `P${page.pageNumber} edit saved`
515 ),
516 progress: 90,
517 currentPage: page.pageNumber,
518 totalPages: selectedPageRefs.length,
519 ...payload
520 }
521 })
522 }
523 },
524 onPageFailed: async (result) => {
525 const pageRef = selectedPageRefs.find((p) => p.pageId === result.pageId)
526 if (!pageRef) return
527 const outlineItem = outlineItemByPageId.get(result.pageId)
528 await db.upsertGenerationPage({
529 runId: context.runId,
530 sessionId: context.sessionId,
531 pageId: result.pageId,
532 pageNumber: pageRef.pageNumber,
533 title: pageRef.title,
534 contentOutline: outlineItem?.contentOutline || '',
535 layoutIntent: outlineItem?.layoutIntent,
536 htmlPath: pageRef.htmlPath,
537 status: 'failed',
538 error: result.reason,
539 retryCount: result.retryCount
540 })
541 const existing = existingBySlug.get(result.pageId)
542 await db.upsertSessionPage({
543 id: existing?.id || pageRef.id || nanoid(),
544 sessionId: context.sessionId,
545 legacyPageId:
546 existing?.legacy_page_id || (result.pageId.match(/^page-\d+$/) ? result.pageId : null),
547 fileSlug: result.pageId,
548 pageNumber: pageRef.pageNumber,
549 title: pageRef.title,
550 htmlPath: pageRef.htmlPath,
551 status: existing?.status || 'failed',
552 error: existing?.error || null
553 })
554 emitEditChunk({
555 type: 'page_failed',
556 payload: {
557 runId: context.runId,
558 stage: 'editing',
559 label: progressText(context.appLocale, 'failed'),
560 progress: 90,
561 currentPage: pageRef.pageNumber,
562 totalPages: selectedPageRefs.length,
563 pageNumber: pageRef.pageNumber,
564 pageId: pageRef.pageId,
565 title: pageRef.title,
566 htmlPath: pageRef.htmlPath,
567 error: result.reason
568 }
569 })
570 }
571 })
572 } catch (error) {
573 const message =
574 error instanceof Error && error.message.length > 0 ? error.message : 'Deck edit failed'
575 log.error('[generate:start] deck edit batch flow aborted', {
576 sessionId: context.sessionId,
577 runId: context.runId,
578 message
579 })
580 await db.updateGenerationRunStatus(context.runId, 'failed', message)
581 throw error
582 }
583
584 const completedBatchResults = batchResults.filter(
585 (result): result is DeckEditCompletedBatch => result.status === 'completed'
586 )
587 const failedBatchResults = batchResults.filter(
588 (result): result is DeckEditFailedBatch => result.status === 'failed'
589 )
590 const changedPageIdSet = new Set(
591 completedBatchResults.flatMap((r) => r.changedPages.map((p) => p.pageId))
592 )
593
594 const remainingFailedPageInfoById = resolveRemainingFailedPageInfo({
595 previousFailures: failedPageInfoById,
596 failedResults: failedBatchResults,
597 completedPageIds: changedPageIdSet,
598 pageRefs
599 })
600 const changedPages = completedBatchResults.flatMap((r) => r.changedPages)
601 const failedPageLabels = failedBatchResults.map((item) => {
602 const page = pageRefs.find((ref) => ref.pageId === item.pageId)
603 return uiText(
604 context.appLocale,
605 `第${page?.pageNumber || item.pageId}页`,
606 page?.pageNumber ? `page ${page.pageNumber}` : item.pageId
607 )
608 })
609 const summaryArgs = {
610 context,
611 changedPages,
612 editScope: 'deck' as const,
613 failedPageLabels
614 }
615 const fallbackEditSummary = buildLocalSuccessfulEditSummary(summaryArgs)
616
617 await db.updateSessionMetadata(context.sessionId, {
618 lastRunId: context.runId,
619 entryMode: 'multi_page',
620 indexPath,
621 projectId: context.projectId
622 })
623 await db.updateProjectStatus(context.projectId, 'draft')
624 await db.updateSessionStatus(
625 context.sessionId,
626 remainingFailedPageInfoById.size > 0 ? 'failed' : 'completed'
627 )
628 const runStatus =
629 failedBatchResults.length === 0
630 ? 'completed'
631 : completedBatchResults.length > 0
632 ? 'partial'
633 : 'failed'
634 const failedDetails = failedBatchResults
635 .map((page) => `${page.pageId}:${page.reason}`)
636 .join(';')
637 await db.updateGenerationRunStatus(
638 context.runId,
639 runStatus,
640 failedBatchResults.length > 0 ? failedDetails : null
641 )
642 if (changedPageIdSet.size > 0) {
643 await ctx.history.recordOperation({
644 sessionId: context.sessionId,
645 projectDir,
646 type: 'edit',
647 scope: 'deck',
648 prompt: context.userMessage,
649 metadata: {
650 runId: context.runId,
651 changedPageIds: Array.from(changedPageIdSet),
652 selectPageIds: selectedPageIds,
653 failedPageIds: failedBatchResults.map((page) => page.pageId),
654 failedPageReasons: Object.fromEntries(
655 failedBatchResults.map((page) => [page.pageId, page.reason])
656 )
657 }
658 })
659 }
660 await emitSuccessfulEditSummary(context, fallbackEditSummary, emitAssistant)
661 log.info('[generate:start] deck all-page edit completed', {
662 sessionId: context.sessionId,
663 styleId: context.styleId,
664 changedPages: Array.from(changedPageIdSet),
665 failedPages: failedBatchResults.map((page) => page.pageId),
666 remainingFailedPages: Array.from(remainingFailedPageInfoById.keys()),
667 batchCount: batchResults.length
668 })
669 if (runStatus === 'failed') {
670 emitEditChunk({
671 type: 'run_error',
672 payload: {
673 runId: context.runId,
674 message: failedDetails || fallbackEditSummary,
675 completedPageCount: 0,
676 failedPageCount: failedBatchResults.length
677 }
678 })
679 } else {
680 emitEditChunk({
681 type: 'run_completed',
682 payload: {
683 runId: context.runId,
684 totalPages: selectedPageRefs.length,
685 completedPageCount: changedPageIdSet.size,
686 failedPageCount: failedBatchResults.length
687 }
688 })
689 }
690 }
691
691 lines TYPESCRIPT