返回 oh-my-ppt
generation-user.ts
根目录 / src / main / agent-runtime / prompt / composers / generation-user.ts
1 import type { SessionDeckGenerationContext } from '../../agent/types'
2 import { formatLayoutIntentPrompt } from '@shared/layout-intent'
3 import { isSectionAgendaOutline, type PageReferenceContext } from '@shared/generation'
4 import { getLayoutMasterTemplate } from '@shared/layout-master'
5 import { buildGenerationImageIntentRules } from './generation-image-intent-rules'
6 import { CHART_SKILL_NAME, formatSkillUsageRequirement } from '../../../product-skills/contract'
7 import {
8 buildCanvasScenarioContentRules,
9 buildCanvasScenarioDeliveryGuard,
10 buildCanvasScenarioExpansionRules,
11 buildLayoutCollisionRules,
12 buildPageSemanticStructure,
13 buildCanvasConstraints,
14 buildReferenceRangeContentBoundaryRules,
15 CONTENT_LANGUAGE_RULES,
16 FRONTEND_CAPABILITIES,
17 SOURCE_DOCUMENT_FACT_RULE,
18 SOURCE_DOCUMENT_LOCATE_THEN_READ_RULE,
19 SOURCE_DOCUMENT_READ_STRATEGY,
20 SOURCE_GROUNDED_EXPANSION_RULES,
21 SOURCE_UNSUPPORTED_CLAIMS,
22 STABLE_HTML_FRAGMENT_PROTOCOL
23 } from './shared'
24 import { buildCanvasScenarioBrief, resolveCanvasScenario } from './canvas-scenario'
25
26 export function buildSinglePageGenerationPrompt(args: {
27 topic: string
28 deckTitle: string
29 pageId: string
30 pageNumber: number
31 pageTitle: string
32 pageOutline: string
33 slideSize: import('@shared/slide-size').SlideSizePreset
34 layoutIntent?: SessionDeckGenerationContext['outlineItems'][number]['layoutIntent']
35 layoutId?: SessionDeckGenerationContext['outlineItems'][number]['layoutId']
36 layoutPrompt?: SessionDeckGenerationContext['outlineItems'][number]['layoutPrompt']
37 sourceDocumentPaths?: string[]
38 referenceDocumentPath?: string
39 pageReferenceContext?: PageReferenceContext
40 referenceDocumentSnippets?: string
41 isRetryMode?: boolean
42 writeToolName?: 'update_single_page_file' | 'update_template_page_file'
43 retryContext?: {
44 attempt: number
45 maxRetries: number
46 previousError: string
47 }
48 visualEnabled?: boolean
49 hasStyleImageDirection?: boolean
50 }): string {
51 const writeToolName = args.writeToolName || 'update_single_page_file'
52 const previousError = args.retryContext?.previousError || ''
53 const shouldMentionChartFix = /chart|canvas|PPT\.createChart/i.test(previousError)
54 const shouldMentionWriteToolFix =
55 /页面未写入|没有成功调用|not written|update_single_page_file|update_template_page_file|占位|placeholder/i.test(
56 previousError
57 )
58 const shouldMentionTemplateSkeletonFix =
59 /模板骨架|skeleton|background\/decorative|背景\/装饰资源|CSS url|SVG image|local asset/i.test(
60 previousError
61 )
62 const isSectionAgendaPage =
63 args.pageReferenceContext?.isSectionAgenda || isSectionAgendaOutline(args.pageOutline || '')
64 const referenceDocumentPath =
65 args.pageReferenceContext &&
66 args.referenceDocumentPath === args.pageReferenceContext.referenceDocumentPath &&
67 args.sourceDocumentPaths?.includes(args.referenceDocumentPath)
68 ? args.referenceDocumentPath
69 : undefined
70 const referenceTextLocked = Boolean(referenceDocumentPath && args.pageReferenceContext)
71 const retryInstructions = args.retryContext
72 ? [
73 '',
74 'Retry fixes to prioritize:',
75 `- This is retry ${args.retryContext.attempt}/${args.retryContext.maxRetries}.`,
76 `- Previous failure: ${previousError}`,
77 '- Output only a complete creative page fragment. The write tool will add section/main/content semantics when they are missing. Do not output a full document, page shell, or runtime scripts.',
78 shouldMentionWriteToolFix
79 ? `- The previous attempt did not write the target page. You must call ${writeToolName}(pageId="${args.pageId}", content=...) before any final response; do not only describe the HTML in the final response.`
80 : '',
81 shouldMentionTemplateSkeletonFix
82 ? '- The previous attempt dropped template skeleton resources. Reread the target template page, find the missing local asset references from the error, and include the corresponding background/decorative layers in the next write.'
83 : '',
84 '- Before calling the write tool, mentally validate that the main containers are closed and that no tag is left unfinished at the end.',
85 '- If the previous issue was unclosed tags, do not patch the broken fragment. Rewrite a simpler, self-contained fragment from scratch with balanced tags and no page shell (section[data-page-scaffold], main[data-role="content"], or runtime frame). Keep the hierarchy shallow and avoid unnecessary wrappers or modules.',
86 '- If the previous issue was page shell structure, do not include .ppt-page-root, .ppt-page-content, .ppt-page-fit-scope, or data-ppt-guard-root anywhere, including CSS selectors, class names, scripts, and comments.',
87 shouldMentionChartFix
88 ? `- The previous issue involved chart API usage. Before repairing or writing chart code: ${formatSkillUsageRequirement(CHART_SKILL_NAME)}`
89 : ''
90 ].filter(Boolean)
91 : []
92 const canUseSourceDocuments =
93 (!isSectionAgendaPage || referenceTextLocked) && Boolean(args.sourceDocumentPaths?.length)
94 let sourceDocumentInstructions: string[] = []
95 if (canUseSourceDocuments && referenceDocumentPath) {
96 sourceDocumentInstructions = [
97 '',
98 args.referenceDocumentSnippets?.trim() || '',
99 '',
100 buildReferenceRangeContentBoundaryRules(referenceDocumentPath, args.pageReferenceContext),
101 '- Content points and retrieved snippets are for locating the raw source passage only. They are not permission to render planning metadata or normalized snippet text.',
102 args.isRetryMode
103 ? '- This is a failed-slide retry. Keep the page within its Reference Range Content Boundary; repair layout without introducing outside facts.'
104 : ''
105 ].filter(Boolean)
106 } else if (canUseSourceDocuments && args.sourceDocumentPaths) {
107 sourceDocumentInstructions =
108 args.referenceDocumentSnippets && args.referenceDocumentSnippets.trim().length > 0
109 ? [
110 '',
111 args.referenceDocumentSnippets.trim(),
112 '',
113 'Source document requirements:',
114 '- This slide already has program-side retrieved snippets.',
115 `- Source document paths: ${args.sourceDocumentPaths.join(', ')}`,
116 SOURCE_DOCUMENT_READ_STRATEGY,
117 SOURCE_DOCUMENT_FACT_RULE,
118 SOURCE_GROUNDED_EXPANSION_RULES,
119 args.isRetryMode
120 ? '- This is a failed-slide retry. Match source material only around this slide title and content points; do not reconstruct the whole deck outline.'
121 : ''
122 ].filter(Boolean)
123 : [
124 '',
125 'Source document requirements:',
126 `- Source document paths: ${args.sourceDocumentPaths.join(', ')}`,
127 '- No retrieved snippets matched this slide.',
128 SOURCE_DOCUMENT_LOCATE_THEN_READ_RULE,
129 '- First extract keywords, business objects, time points, system names, and metrics from this slide title and content points; then match relevant source passages.',
130 '- Do not copy the whole document indiscriminately.',
131 SOURCE_DOCUMENT_FACT_RULE,
132 SOURCE_GROUNDED_EXPANSION_RULES,
133 args.isRetryMode
134 ? '- This is a failed-slide retry. Match source material only around this slide title and content points; do not reconstruct the whole deck outline.'
135 : ''
136 ].filter(Boolean)
137 }
138 const hasSourceRange = Boolean(args.pageReferenceContext) || /Source range:\s*lines\s+\d+\s*-\s*\d+/i.test(args.pageOutline || '')
139 const canvasScenario = resolveCanvasScenario(args.slideSize)
140 const sourceRangeInstructions =
141 (!isSectionAgendaPage || referenceTextLocked) &&
142 args.sourceDocumentPaths &&
143 args.sourceDocumentPaths.length > 0 &&
144 hasSourceRange
145 ? [
146 '',
147 'Range-bound source reading:',
148 '- Content points include a Source range. Before writing this slide, inspect that source heading/range first through the source-reading skill.',
149 '- Use retrieved snippets as an index only. If snippets are missing or broad, the Source range remains the primary content boundary.',
150 referenceTextLocked
151 ? '- Rephrase and reorganize for the presentation when useful, but keep every displayed fact, qualifier, relationship, and conclusion within the selected range.'
152 : '- Do not pull facts from unrelated sections just because they match keywords.'
153 ]
154 : []
155 const sectionAgendaInstructions = isSectionAgendaPage
156 ? referenceTextLocked
157 ? [
158 '',
159 'Section agenda page requirements:',
160 '- The page is governed by the Reference Range Content Boundary. Use the source heading/range for chapter context.',
161 args.pageReferenceContext?.agendaItems?.length
162 ? `- Use only these recorded Agenda items as child topics: ${JSON.stringify(args.pageReferenceContext.agendaItems)}.`
163 : '- This legacy agenda has no recorded Agenda items; retain its existing agenda behavior without inferring child topics from document body.',
164 '- Do not add fixed visible labels such as “Agenda”, “目录”, “本章包括”, or “章节导航” unless the source itself contains them.'
165 ]
166 : [
167 '',
168 'Section agenda page requirements:',
169 '- This slide is a chapter agenda/table-of-contents page.',
170 '- Use only the child topic names already listed in Content points.',
171 '- Do not inspect, retrieve, cite, summarize, or expand from the source document for this slide.',
172 '- Keep it as a presentation agenda: chapter title plus concise child-topic list.'
173 ]
174 : []
175 const contentDecisionInstructions = referenceTextLocked
176 ? [
177 '',
178 'Reference-bound decision before writing:',
179 '- The selected range supplies the page facts. You may summarize repetition, regroup content, and use neutral structural copy, but may not introduce outside facts or change the source meaning.',
180 '- Planning text, slide titles, and style guidance may determine the presentation. They must not create unsupported numbers, dates, proper nouns, causal claims, or business conclusions.',
181 '- When the source is dense, prioritize hierarchy, grouping, and visualization; then use compact internal layout while preserving actual nonzero gaps between independent modules. Use bounded internal-module scaling only as a final measure. Never scale the page root, section/page shell, `main[data-role="content"]`, or canvas.'
182 ]
183 : [
184 '',
185 'Required content enrichment decision before writing:',
186 '- First use the Canvas scenario rules to decide the page form and focal message; then use the scenario expansion rules only to decide whether the content itself needs enrichment or optimization.',
187 '- If the content points are only a title, one short sentence, or 1-2 seed phrases, the page is thin: enrich the warranted structure before writing the final content.',
188 '- If the content points already contain enough facts, the page is not thin: group and compress instead of adding more visible modules.',
189 '- This content decision happens before animation and final HTML; animation is downstream only and must follow the current canvas scenario, source grounding, and warranted content enrichment.',
190 '',
191 'Expansion selection guardrails:',
192 '- Treat content points as short seed phrases, not as a checklist that must become one visible card/row per point. Decide which points are primary, grouped support, compact annotations, or lower-priority detail based on the slide title, source range, and available space.',
193 '- When source documents are present, expansion must be source-grounded through SOURCE_GROUNDED_EXPANSION_RULES: if inspected material is thin, enrich the slide from inspected material; if it is dense, summarize and group.',
194 `- Do not add generic industry framing, unsupported ${SOURCE_UNSUPPORTED_CLAIMS}, or polished-sounding conclusions that are absent from the source document.`,
195 '- Do not duplicate the same source facts in multiple large modules. If a fact appears in a timeline/table/chart, do not repeat it again as a separate summary card unless it is the single hero message of the slide.',
196 '- When there are many same-level points, preserve the main meaning by grouping related points and surfacing only the amount that fits a real slide with actual gaps between independent modules.',
197 '- When the user provided an explicit list of same-level topics, keep them distinct only where the layout allows; otherwise group under shared headings instead of creating equal-weight modules for every item.',
198 '- Prefer visualization-friendly expression. When points involve trends, comparisons, or proportions, use charts or data cards when appropriate.'
199 ]
200 return [
201 `Generate and write only this ${canvasScenario.pageName}. Do not modify other pages.`,
202 '',
203 buildCanvasScenarioBrief(args.slideSize),
204 '',
205 buildCanvasScenarioContentRules(args.slideSize, { referenceTextLocked }),
206 '',
207 `Topic: ${args.topic}`,
208 `Deck title: ${args.deckTitle}`,
209 `Target page: ${args.pageId} (slide ${args.pageNumber})`,
210 `Slide title: ${args.pageTitle}`,
211 `Content points: ${args.pageOutline || 'Expand from the topic with moderate information density.'}`,
212 args.layoutIntent ? formatLayoutIntentPrompt(args.layoutIntent) : '',
213 args.layoutId && args.layoutPrompt ? args.layoutPrompt : '',
214 args.layoutId && getLayoutMasterTemplate(args.layoutId)
215 ? buildGenerationImageIntentRules({
216 visualEnabled: args.visualEnabled === true,
217 template: getLayoutMasterTemplate(args.layoutId)!,
218 hasStyleImageDirection: args.hasStyleImageDirection
219 })
220 : '',
221 ...sectionAgendaInstructions,
222 ...sourceDocumentInstructions,
223 ...sourceRangeInstructions,
224 '',
225 CONTENT_LANGUAGE_RULES,
226 '',
227 buildPageSemanticStructure(args.slideSize),
228 '',
229 buildCanvasConstraints(args.slideSize, { referenceTextLocked }),
230 '',
231 buildLayoutCollisionRules(args.slideSize),
232 '',
233 buildCanvasScenarioDeliveryGuard(args.slideSize, { referenceTextLocked }),
234 '',
235 FRONTEND_CAPABILITIES,
236 '',
237 STABLE_HTML_FRAGMENT_PROTOCOL,
238 ...retryInstructions,
239 '',
240 buildCanvasScenarioExpansionRules(args.slideSize, { referenceTextLocked }),
241 '',
242 ...contentDecisionInstructions,
243 '',
244 'Single-slide tool constraints:',
245 `- Required action: call ${writeToolName}(pageId="${args.pageId}", content=complete creative page fragment).`,
246 `- This is not optional. A final text response without a successful ${writeToolName} tool call means the slide is not generated.`,
247 '- Do not call update_page_file. In this single-slide run it is intentionally not available.',
248 writeToolName === 'update_template_page_file'
249 ? '- Do not call update_single_page_file. This template run exposes update_template_page_file instead.'
250 : '',
251 '- content must be a complete creative page fragment. The tool will wrap it with section[data-page-scaffold], main[data-role="content"], editable data-block-id attributes, and the runtime page frame when needed.',
252 '- The content must not contain <!doctype>, <html>, <head>, <body>, .ppt-page-root, .ppt-page-content, .ppt-page-fit-scope, or data-ppt-guard-root.',
253 '- The content must be complete and balanced: close your main layout containers and leave no unfinished trailing tags.',
254 '- After the tool call succeeds, final response should be a short summary only. Do not paste the HTML in the final response.',
255 '- Do not modify other pages.',
256 '',
257 'Tool context (pre-injected):',
258 `- Target file: ${args.pageId}.html (virtual path: /${args.pageId}.html)`,
259 '- Agent workspace root: /'
260 ].join('\n')
261 }
262
262 lines TYPESCRIPT