返回 oh-my-ppt
context.ts
根目录 / src / main / generation / context.ts
1 import fs from 'fs'
2 import path from 'path'
3 import type {
4 FontSelection,
5 GenerateStartPayload,
6 SelectedElementRuntimeContext,
7 SessionPageEditPlan,
8 SourceDocumentPlan
9 } from '@shared/generation'
10 import {
11 MAX_SELECTED_PAGES,
12 MAX_STYLE_SWITCH_PAGES,
13 SELECTED_ELEMENT_CONTEXT_COMPUTED_STYLE_PROPERTIES,
14 normalizeAnimationPreferences,
15 normalizeFontSelection,
16 normalizeSessionPageEditPlan,
17 normalizeSelectPageIds
18 } from '@shared/generation'
19 import type { AnimationPreferencesPayload } from '@shared/generation'
20 import type { ModelTimeoutProfile } from '@shared/model-timeout'
21 import type { AgentManager } from '../agent-runtime/agent'
22 import type { ModelRuntimeConfig } from '../agent-runtime/model'
23 import type { GenerateChatType } from './types'
24 import type { PPTDatabase, SessionStyleSnapshotRow } from '../db/database'
25 import { requireSessionSlideSize, type SlideSizePreset } from '@shared/slide-size'
26 import type { RuntimeCredentials } from '../ipc/runtime/credentials'
27 import type { RuntimeLocalFiles } from '../ipc/runtime/local-files'
28 import type { RuntimeEmitters } from '../ipc/runtime/runtime-emitters'
29 import type { SessionProjectResolver } from '../ipc/runtime/session-project'
30 import type { SessionScaffold } from '../ipc/runtime/session-scaffold'
31 import type { SessionRunStateStore } from '../ipc/runtime/session-run-state'
32
33 export { resolveSourceDocuments } from './source-documents'
34 import { resolveGlobalModelTimeouts, resolveModelConfigForTask } from '../config/model-config-utils'
35 import {
36 ensureHistoryBaselineSafe,
37 recordHistoryOperationStrict
38 } from '../history/git-history-service'
39 import { extractOutlineTitles, parseJsonObject } from '../ipc/utils'
40 import { sourcePlanFromSkeletonRows } from './source-plan'
41
42 export type GenerationDbPort = Pick<
43 PPTDatabase,
44 | 'addMessage'
45 | 'createGenerationRun'
46 | 'createGenerationRunWithSessionJob'
47 | 'createProject'
48 | 'getActiveModelConfig'
49 | 'getAllSettings'
50 | 'getGenerationRun'
51 | 'getLatestSessionJob'
52 | 'getModelConfig'
53 | 'getOrCreateSessionStyleSnapshot'
54 | 'getProject'
55 | 'getSession'
56 | 'getSetting'
57 | 'listActiveSessionJobs'
58 | 'listGenerationPages'
59 | 'listLatestGenerationPageSnapshot'
60 | 'listSessionPages'
61 | 'listSourcePageSkeletons'
62 | 'updateGenerationRunStatus'
63 | 'updateProjectStatus'
64 | 'updateSessionDesignContract'
65 | 'updateSessionJobStatus'
66 | 'updateSessionMetadata'
67 | 'updateSessionStatus'
68 | 'upsertGenerationPage'
69 | 'upsertSessionPage'
70 >
71
72 export type GenerationTuning = {
73 plannerTemperature: number
74 designContractTemperature: number
75 pageGenerationTemperature: number
76 pageEditWithSelectorTemperature: number
77 pageEditDefaultTemperature: number
78 }
79
80 export type GenerationAgentManager = Pick<
81 AgentManager,
82 | 'clearCachedAgent'
83 | 'ensureSession'
84 | 'getSession'
85 | 'removePageAgent'
86 | 'removeSession'
87 | 'setAgent'
88 | 'setPageAgent'
89 >
90
91 export type GenerationHistory = {
92 ensureBaseline(sessionId: string, projectDir: string): Promise<void>
93 recordOperation(args: Parameters<typeof recordHistoryOperationStrict>[1]): Promise<void>
94 }
95
96 /**
97 * The complete set of capabilities Generation may use. It deliberately owns no
98 * Electron objects and does not inherit the broad IPC compatibility facade.
99 */
100 export type GenerationContext = {
101 db: GenerationDbPort
102 agentManager: GenerationAgentManager
103 modelRuntime: ModelRuntimeConfig
104 sessionRuns: SessionRunStateStore
105 runtimeEmitters: Pick<
106 RuntimeEmitters,
107 | 'emitGenerateChunk'
108 | 'emitRuntimeJobStarted'
109 | 'emitRuntimeJobTerminal'
110 | 'emitSessionRunLifecycle'
111 | 'createDeckProgressEmitter'
112 >
113 sessionProject: Pick<
114 SessionProjectResolver,
115 'getPageSourceUrl' | 'resolveSessionProjectDir' | 'validateProjectIndexHtml'
116 >
117 localFiles: Pick<
118 RuntimeLocalFiles,
119 'assertPathInAllowedRoots' | 'formatImagePathsForPrompt' | 'resolveStoragePath'
120 >
121 sessionScaffold: Pick<SessionScaffold, 'ensureSessionAssets' | 'scaffoldProjectFiles'>
122 credentials: Pick<RuntimeCredentials, 'decryptApiKey'>
123 history: GenerationHistory
124 tuning: GenerationTuning
125 }
126
127 /**
128 * IPC composition helper. The input is structural on purpose so the setup
129 * layer can pass its compatibility facade without Generation importing it.
130 */
131 export type GenerationContextAssembly = Omit<GenerationContext, 'history' | 'tuning'> & {
132 db: PPTDatabase
133 PLANNER_TEMPERATURE: number
134 DESIGN_CONTRACT_TEMPERATURE: number
135 PAGE_GENERATION_TEMPERATURE: number
136 PAGE_EDIT_WITH_SELECTOR_TEMPERATURE: number
137 PAGE_EDIT_DEFAULT_TEMPERATURE: number
138 }
139
140 export const createGenerationContext = (args: GenerationContextAssembly): GenerationContext => ({
141 db: args.db,
142 agentManager: args.agentManager,
143 modelRuntime: args.modelRuntime,
144 sessionRuns: args.sessionRuns,
145 runtimeEmitters: args.runtimeEmitters,
146 sessionProject: args.sessionProject,
147 localFiles: args.localFiles,
148 sessionScaffold: args.sessionScaffold,
149 credentials: args.credentials,
150 history: {
151 ensureBaseline: (sessionId, projectDir) =>
152 ensureHistoryBaselineSafe(args.db, sessionId, projectDir),
153 recordOperation: (operation) => recordHistoryOperationStrict(args.db, operation)
154 },
155 tuning: {
156 plannerTemperature: args.PLANNER_TEMPERATURE,
157 designContractTemperature: args.DESIGN_CONTRACT_TEMPERATURE,
158 pageGenerationTemperature: args.PAGE_GENERATION_TEMPERATURE,
159 pageEditWithSelectorTemperature: args.PAGE_EDIT_WITH_SELECTOR_TEMPERATURE,
160 pageEditDefaultTemperature: args.PAGE_EDIT_DEFAULT_TEMPERATURE
161 }
162 })
163
164 export type CommonGenerationContext = {
165 session: Awaited<ReturnType<GenerationDbPort['getSession']>>
166 sessionRecord: Record<string, unknown>
167 previousSessionStatus: string
168 runId: string
169 provider: string
170 apiKey: string
171 model: string
172 modelConfigId?: string
173 modelConfigName?: string
174 runModel?: string
175 providerBaseUrl: string
176 maxTokens: number
177 modelRuntime: ModelRuntimeConfig
178 modelTimeouts: Record<ModelTimeoutProfile, number>
179 projectDir: string
180 abortSignal: AbortSignal
181 styleId: string
182 styleSnapshot: SessionStyleSnapshotRow
183 styleSkill: {
184 preset: {
185 id: string
186 label: string
187 aliases: string[]
188 description: string
189 fallbackPrompt: string
190 }
191 prompt: string
192 }
193 styleSkillPrompt: string
194 styleKey: string
195 styleName: string
196 styleVersion: string
197 slideSize: SlideSizePreset
198 topic: string
199 deckTitle: string
200 appLocale: 'zh' | 'en'
201 fontSelection: FontSelection
202 sourcePlan: SourceDocumentPlan | null
203 projectId: string
204 }
205
206 /**
207 * Run-scoped identity and cancellation supplied by JobCoordinator. Generation
208 * resolves all expensive context only after this lease has been acquired.
209 */
210 export type RuntimeJobExecutionContext = {
211 runId: string
212 abortSignal: AbortSignal
213 }
214
215 export type NormalizedGenerateInput = {
216 sessionId: string
217 modelConfigId?: string
218 rawUserMessage: string
219 rawImagePaths: string[]
220 rawVideoPaths: string[]
221 rawDocPaths: string[]
222 requestedType?: 'deck' | 'page'
223 resetVisualStyle: boolean
224 persistUserMessage: boolean
225 clientMessageId?: string
226 selectedPageId?: string
227 selectPageIds: string[]
228 htmlPath?: string
229 selector?: string
230 elementTag?: string
231 elementText?: string
232 selectedElementContext?: SelectedElementRuntimeContext
233 chatType: GenerateChatType
234 chatPageId?: string
235 animationPreferences: AnimationPreferencesPayload | null
236 autoApply: boolean
237 approvedPlan?: SessionPageEditPlan
238 failedRunId?: string
239 }
240
241 const MAX_SELECTED_ELEMENT_CONTEXT_ENTRIES = 40
242 const MAX_SELECTED_ELEMENT_CONTEXT_CLASSES = 24
243 const MAX_SELECTED_ELEMENT_CONTEXT_VALUE_LENGTH = 480
244 const PROMPT_SAFE_COMPUTED_STYLE_PROPERTIES = new Set<string>(
245 SELECTED_ELEMENT_CONTEXT_COMPUTED_STYLE_PROPERTIES
246 )
247
248 const normalizeSelectedElementContextValue = (value: unknown, maxLength = MAX_SELECTED_ELEMENT_CONTEXT_VALUE_LENGTH): string =>
249 String(value ?? '')
250 .replace(/\s+/g, ' ')
251 .trim()
252 .slice(0, maxLength)
253
254 const isSelectedElementContextAttributeName = (value: string): boolean => {
255 const name = value.toLowerCase()
256 return (
257 Boolean(name) &&
258 !name.startsWith('on') &&
259 name !== 'style' &&
260 name !== 'srcdoc' &&
261 !name.startsWith('data-arcsin1-presentation-editor-')
262 )
263 }
264
265 export function normalizeSelectedElementRuntimeContext(
266 value: unknown
267 ): SelectedElementRuntimeContext | undefined {
268 if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
269 const input = value as Record<string, unknown>
270 const attributes: Record<string, string> = {}
271 if (input.attributes && typeof input.attributes === 'object' && !Array.isArray(input.attributes)) {
272 for (const [key, rawValue] of Object.entries(input.attributes)) {
273 if (Object.keys(attributes).length >= MAX_SELECTED_ELEMENT_CONTEXT_ENTRIES) break
274 const name = normalizeSelectedElementContextValue(key, 100).toLowerCase()
275 if (!isSelectedElementContextAttributeName(name)) continue
276 attributes[name] = normalizeSelectedElementContextValue(rawValue)
277 }
278 }
279
280 const inlineStyle: NonNullable<SelectedElementRuntimeContext['inlineStyle']> = {}
281 if (input.inlineStyle && typeof input.inlineStyle === 'object' && !Array.isArray(input.inlineStyle)) {
282 for (const [key, rawDeclaration] of Object.entries(input.inlineStyle)) {
283 if (Object.keys(inlineStyle).length >= MAX_SELECTED_ELEMENT_CONTEXT_ENTRIES) break
284 const property = normalizeSelectedElementContextValue(key, 100).toLowerCase()
285 if (!/^(?:--)?[a-z][a-z0-9-]*$/i.test(property)) continue
286 const declaration =
287 rawDeclaration && typeof rawDeclaration === 'object' && !Array.isArray(rawDeclaration)
288 ? (rawDeclaration as Record<string, unknown>)
289 : null
290 if (!declaration) continue
291 inlineStyle[property] = {
292 value: normalizeSelectedElementContextValue(declaration.value),
293 priority: declaration.priority === 'important' ? 'important' : ''
294 }
295 }
296 }
297
298 const computedStyle: Record<string, string> = {}
299 if (input.computedStyle && typeof input.computedStyle === 'object' && !Array.isArray(input.computedStyle)) {
300 for (const [key, rawValue] of Object.entries(input.computedStyle)) {
301 const property = normalizeSelectedElementContextValue(key, 100).toLowerCase()
302 if (!PROMPT_SAFE_COMPUTED_STYLE_PROPERTIES.has(property)) continue
303 const normalizedValue = normalizeSelectedElementContextValue(rawValue)
304 if (normalizedValue) computedStyle[property] = normalizedValue
305 }
306 }
307
308 const classList = Array.isArray(input.classList)
309 ? input.classList
310 .map((item) => normalizeSelectedElementContextValue(item, 100))
311 .filter(
312 (item) =>
313 Boolean(item) &&
314 !item.startsWith('arcsin1-presentation-editor-') &&
315 !item.startsWith('ppt-inspector-')
316 )
317 .slice(0, MAX_SELECTED_ELEMENT_CONTEXT_CLASSES)
318 : []
319 const boundsInput =
320 input.bounds && typeof input.bounds === 'object' && !Array.isArray(input.bounds)
321 ? (input.bounds as Record<string, unknown>)
322 : null
323 const boundsValues = boundsInput
324 ? [boundsInput.x, boundsInput.y, boundsInput.width, boundsInput.height].map(Number)
325 : []
326 const bounds =
327 boundsValues.length === 4 && boundsValues.every(Number.isFinite)
328 ? {
329 x: Math.round(Math.max(-100_000, Math.min(100_000, boundsValues[0])) * 100) / 100,
330 y: Math.round(Math.max(-100_000, Math.min(100_000, boundsValues[1])) * 100) / 100,
331 width: Math.round(Math.max(0, Math.min(100_000, boundsValues[2])) * 100) / 100,
332 height: Math.round(Math.max(0, Math.min(100_000, boundsValues[3])) * 100) / 100
333 }
334 : undefined
335
336 if (
337 classList.length === 0 &&
338 Object.keys(attributes).length === 0 &&
339 Object.keys(inlineStyle).length === 0 &&
340 Object.keys(computedStyle).length === 0 &&
341 !bounds
342 ) {
343 return undefined
344 }
345 return {
346 ...(classList.length > 0 ? { classList } : {}),
347 ...(Object.keys(attributes).length > 0 ? { attributes } : {}),
348 ...(Object.keys(inlineStyle).length > 0 ? { inlineStyle } : {}),
349 ...(Object.keys(computedStyle).length > 0 ? { computedStyle } : {}),
350 ...(bounds ? { bounds } : {})
351 }
352 }
353
354 export function normalizeGeneratePayload(payload: unknown): NormalizedGenerateInput {
355 const input = payload as GenerateStartPayload
356 const sessionId = String(input?.sessionId || '').trim()
357 const modelConfigId =
358 typeof input?.modelConfigId === 'string' && input.modelConfigId.trim().length > 0
359 ? input.modelConfigId.trim()
360 : undefined
361 const rawUserMessage = typeof input?.userMessage === 'string' ? input.userMessage : ''
362 const rawImagePaths = Array.isArray(input?.imagePaths)
363 ? input.imagePaths
364 .map((item) => String(item || '').trim())
365 .filter((item) => item.startsWith('./images/'))
366 .slice(0, 10)
367 : []
368 const rawVideoPaths = Array.isArray(input?.videoPaths)
369 ? input.videoPaths
370 .map((item) => String(item || '').trim())
371 .filter((item) => item.startsWith('./videos/'))
372 .slice(0, 10)
373 : []
374 const rawDocPaths = Array.isArray(input?.docPaths)
375 ? input.docPaths
376 .map((item) => String(item || '').trim())
377 .filter(Boolean)
378 .slice(0, 1)
379 : []
380 const requestedType =
381 input?.type === 'page' ? 'page' : input?.type === 'deck' ? 'deck' : undefined
382 const resetVisualStyle = input?.resetVisualStyle === true
383 const persistUserMessage = input?.persistUserMessage !== false
384 const rawClientMessageId =
385 typeof input?.clientMessageId === 'string' ? input.clientMessageId.trim() : ''
386 const clientMessageId =
387 /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
388 rawClientMessageId
389 )
390 ? rawClientMessageId
391 : undefined
392 const selectedPageId =
393 typeof input?.selectedPageId === 'string' && input.selectedPageId.trim().length > 0
394 ? input.selectedPageId.trim()
395 : undefined
396 const selectPageIds = normalizeSelectPageIds(
397 input?.selectPageIds,
398 resetVisualStyle ? MAX_STYLE_SWITCH_PAGES : MAX_SELECTED_PAGES
399 )
400 const htmlPath = typeof input?.htmlPath === 'string' ? input.htmlPath : undefined
401 const selector =
402 typeof input?.selector === 'string' && input.selector.trim().length > 0
403 ? input.selector.trim()
404 : undefined
405 const elementTag =
406 typeof input?.elementTag === 'string' && input.elementTag.trim().length > 0
407 ? input.elementTag.trim()
408 : undefined
409 const elementText =
410 typeof input?.elementText === 'string' && input.elementText.trim().length > 0
411 ? input.elementText.trim()
412 : undefined
413 const selectedElementContext = selector
414 ? normalizeSelectedElementRuntimeContext(input?.selectedElementContext)
415 : undefined
416 const chatType: GenerateChatType = input?.chatType === 'page' ? 'page' : 'main'
417 const chatPageId =
418 chatType === 'page' && typeof input?.chatPageId === 'string' && input.chatPageId.trim().length > 0
419 ? input.chatPageId.trim()
420 : undefined
421 const animationPreferences = normalizeAnimationPreferences(input?.animationPreferences)
422 const autoApply = input?.autoApply === true
423 const approvedPlan = normalizeSessionPageEditPlan(input?.approvedPlan)
424 const failedRunIdRaw = (payload as { failedRunId?: unknown } | null)?.failedRunId
425 const failedRunId =
426 typeof failedRunIdRaw === 'string' && failedRunIdRaw.trim().length > 0
427 ? failedRunIdRaw.trim()
428 : undefined
429
430 return {
431 sessionId,
432 modelConfigId,
433 rawUserMessage,
434 rawImagePaths,
435 rawVideoPaths,
436 rawDocPaths,
437 requestedType,
438 resetVisualStyle,
439 persistUserMessage,
440 clientMessageId,
441 selectedPageId,
442 selectPageIds,
443 htmlPath,
444 selector,
445 elementTag,
446 elementText,
447 selectedElementContext,
448 chatType,
449 chatPageId,
450 animationPreferences,
451 autoApply,
452 approvedPlan,
453 failedRunId
454 }
455 }
456
457 export function buildRetryUserMessage(retrySupplementRaw: string): string {
458 const retrySupplement = retrySupplementRaw.trim()
459 return retrySupplement
460 ? [
461 '继续生成本会话中未完成的页面。页面正文、标题、图表标签必须保持与现有页面相同语言。',
462 'Continue generating the unfinished slides in this session. Keep slide text, titles, and chart labels in the same language as existing slides.',
463 'Determine the content language from the existing topic, outline, source materials, existing slides, and the user supplement; do not infer it from this instruction language.',
464 `User supplement:\n${retrySupplement}`
465 ].join('\n')
466 : [
467 '继续生成本会话中未完成的页面。页面正文、标题、图表标签必须保持与现有页面相同语言。',
468 'Continue generating the unfinished slides in this session. Keep slide text, titles, and chart labels in the same language as existing slides.',
469 'Determine the content language from the existing topic, outline, source materials, and existing slides; do not infer it from this instruction language.'
470 ].join('\n')
471 }
472
473 export function buildTotalPages(sessionRecord: Record<string, unknown>): number {
474 const total = Number(sessionRecord.page_count ?? sessionRecord.pageCount)
475 return Math.max(1, Number.isFinite(total) ? Math.floor(total) : 1)
476 }
477
478 export function buildOutlineTitles(rawUserMessage: string): string[] {
479 return extractOutlineTitles(rawUserMessage)
480 }
481
482 function parseJsonArray(value: string): string[] {
483 try {
484 const parsed = JSON.parse(value) as unknown
485 return Array.isArray(parsed) ? parsed.map((item) => String(item || '')).filter(Boolean) : []
486 } catch {
487 return []
488 }
489 }
490
491 export async function resolveCommonContext(
492 ctx: GenerationContext,
493 sessionId: string,
494 modelConfigId?: string,
495 execution?: RuntimeJobExecutionContext
496 ): Promise<CommonGenerationContext> {
497 const { db, agentManager, sessionProject, sessionScaffold } = ctx
498 if (!execution) throw new Error('Runtime job execution context is required')
499
500 const session = await db.getSession(sessionId)
501 if (!session) throw new Error('Session not found')
502 const sessionRecord = session as unknown as Record<string, unknown>
503 const sessionMetadata = parseJsonObject(sessionRecord.metadata ?? sessionRecord.metadata_json)
504 const sourcePlan = sourcePlanFromSkeletonRows(await db.listSourcePageSkeletons(sessionId))
505 const previousSessionStatus = String(sessionRecord.status || 'active')
506
507 const modelConfigContext = {
508 db,
509 decryptApiKey: ctx.credentials.decryptApiKey
510 }
511 const activeModel = await resolveModelConfigForTask(modelConfigContext, {
512 modelConfigId,
513 purpose: 'generation'
514 })
515 const modelTimeouts = await resolveGlobalModelTimeouts({ db })
516 const runModel = JSON.stringify({
517 modelConfigId: activeModel.id,
518 name: activeModel.name,
519 provider: activeModel.provider,
520 model: activeModel.model,
521 baseUrl: activeModel.baseUrl || undefined,
522 maxTokens: activeModel.maxTokens
523 })
524
525 const styleSnapshot = await db.getOrCreateSessionStyleSnapshot(sessionId)
526 const styleId = styleSnapshot.styleId
527 const styleAliases = parseJsonArray(styleSnapshot.aliases)
528 const styleSkill = {
529 preset: {
530 id: styleSnapshot.styleId,
531 label: styleSnapshot.styleName,
532 aliases: styleAliases,
533 description: styleSnapshot.description,
534 fallbackPrompt: styleSnapshot.description
535 ? `Use ${styleSnapshot.styleKey} style: ${styleSnapshot.description}`
536 : `Use ${styleSnapshot.styleKey} style.`
537 },
538 prompt:
539 styleSnapshot.styleSkill?.trim() ||
540 (styleSnapshot.description
541 ? `Use ${styleSnapshot.styleKey} style: ${styleSnapshot.description}`
542 : `Use ${styleSnapshot.styleKey} style.`)
543 }
544
545 const existingProject = await db.getProject(sessionId)
546 if (!existingProject) {
547 const storagePath = await ctx.localFiles.resolveStoragePath()
548 const projectDir = path.join(storagePath, sessionId)
549 if (!fs.existsSync(projectDir)) {
550 fs.mkdirSync(projectDir, { recursive: true })
551 }
552 await db.createProject({
553 session_id: sessionId,
554 title: String(sessionRecord.title || 'Untitled'),
555 output_path: projectDir,
556 root_path: projectDir
557 })
558 }
559 const projectDir = await sessionProject.resolveSessionProjectDir(sessionId)
560 if (!fs.existsSync(projectDir)) {
561 fs.mkdirSync(projectDir, { recursive: true })
562 }
563 await sessionScaffold.ensureSessionAssets(projectDir)
564
565 agentManager.ensureSession({
566 sessionId,
567 provider: activeModel.provider,
568 model: activeModel.model,
569 baseUrl: activeModel.baseUrl,
570 projectDir,
571 modelRuntime: ctx.modelRuntime
572 })
573 const settings = await db.getAllSettings()
574 const appLocale: 'zh' | 'en' = settings.locale === 'en' ? 'en' : 'zh'
575 const projectId = existingProject?.id ?? (await db.getProject(sessionId))?.id
576 if (!projectId) throw new Error('Failed to resolve project for session')
577
578 return {
579 session,
580 sessionRecord,
581 previousSessionStatus,
582 runId: execution.runId,
583 provider: activeModel.provider,
584 apiKey: activeModel.apiKey,
585 model: activeModel.model,
586 modelConfigId: activeModel.id,
587 modelConfigName: activeModel.name,
588 runModel,
589 providerBaseUrl: activeModel.baseUrl,
590 maxTokens: activeModel.maxTokens,
591 modelRuntime: ctx.modelRuntime,
592 modelTimeouts,
593 projectDir,
594 abortSignal: execution.abortSignal,
595 styleId,
596 styleSnapshot,
597 styleSkill,
598 styleSkillPrompt: styleSkill.prompt,
599 styleKey: styleSnapshot.styleKey,
600 styleName: styleSnapshot.styleName,
601 styleVersion: styleSnapshot.version,
602 slideSize: requireSessionSlideSize(sessionRecord),
603 topic: String(sessionRecord.topic || '当前主题'),
604 deckTitle: String(sessionRecord.title || 'OhMyPPT Preview'),
605 appLocale,
606 fontSelection: normalizeFontSelection(sessionMetadata.fontSelection),
607 sourcePlan,
608 projectId
609 }
610 }
611
611 lines TYPESCRIPT