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