返回 oh-my-ppt
chart-rewrite-agent.ts
根目录 / src / main / io / pptx-import / chart-rewrite-agent.ts
1 import fs from 'fs'
2 import path from 'path'
3 import log from 'electron-log/main.js'
4 import { resolveModelTimeoutMs } from '@shared/model-timeout'
5 import { extractJsonBlock, extractModelText, resolveModel } from '../../agent-runtime/model'
6 import type { ModelRuntimeConfig } from '../../agent-runtime/model'
7 import { resolveBuiltinSkillsSourcePath } from '../../product-skills/paths'
8 import type {
9 PptxChartRewriteHandler,
10 PptxChartRewriteRequest,
11 PptxChartRewriteResult
12 } from './types'
13
14 type PptxChartRewriteAgentOptions = {
15 provider: string
16 apiKey: string
17 model: string
18 baseUrl?: string
19 maxTokens?: number
20 modelRuntime?: ModelRuntimeConfig
21 modelTimeoutMs: number
22 maxRewrites?: number
23 }
24
25 type ChartSkillDocs = {
26 skill: string
27 reference: string
28 }
29
30 const DEFAULT_MAX_REWRITES = 6
31 let cachedChartSkillDocs: ChartSkillDocs | null = null
32
33 const readChartSkillDocs = async (): Promise<ChartSkillDocs> => {
34 if (cachedChartSkillDocs) return cachedChartSkillDocs
35 const skillRoot = path.join(resolveBuiltinSkillsSourcePath(), 'oh-my-ppt-chart')
36 const [skill, reference] = await Promise.all([
37 fs.promises.readFile(path.join(skillRoot, 'SKILL.md'), 'utf-8'),
38 fs.promises.readFile(path.join(skillRoot, 'references', 'chart.md'), 'utf-8').catch(() => '')
39 ])
40 cachedChartSkillDocs = { skill, reference }
41 return cachedChartSkillDocs
42 }
43
44 const compactUnknown = (value: unknown, maxLength = 12000): unknown => {
45 const text = JSON.stringify(value, (_key, item) => {
46 if (typeof item === 'string' && item.length > 500) return `${item.slice(0, 500)}...`
47 return item
48 })
49 if (!text || text.length <= maxLength) return value
50 return `${text.slice(0, maxLength)}...`
51 }
52
53 const summarizeChartElement = (request: PptxChartRewriteRequest): Record<string, unknown> => {
54 const record = request.element as unknown as Record<string, unknown>
55 return {
56 chartType: request.element.chartType,
57 barDir: 'barDir' in request.element ? request.element.barDir : undefined,
58 colors: request.element.colors || [],
59 data: compactUnknown('data' in request.element ? request.element.data : null),
60 position: {
61 left: record.left,
62 top: record.top,
63 width: record.width,
64 height: record.height
65 }
66 }
67 }
68
69 export const buildPptxChartRewriteSystemPrompt = (docs: ChartSkillDocs): string => `You are the dedicated PPTX chart parsing agent for Oh My PPT.
70
71 You convert one unsupported PPTX chart element into a safe Chart.js config for the existing importer.
72
73 You MUST follow the bundled product skill below.
74
75 <oh-my-ppt-chart/SKILL.md>
76 ${docs.skill}
77 </oh-my-ppt-chart/SKILL.md>
78
79 <oh-my-ppt-chart/references/chart.md>
80 ${docs.reference}
81 </oh-my-ppt-chart/references/chart.md>
82
83 Importer-specific override:
84 - The PPTX importer owns the HTML frame and MUST preserve its original absolute-positioned style.
85 - Do not rewrite or suggest changing the importer frame style. It must stay exactly like:
86 style="position:absolute; left:...; top:...; width:...; height:...; ..."
87 - Do not output HTML.
88 - Return only a Chart.js config object that can be passed to PPT.createChart(canvasElement, config).
89 - Keep options responsive: true and maintainAspectRatio: false.
90 - Use only Chart.js v4-safe chart types: bar, line, pie, doughnut, radar, polarArea, scatter, bubble.
91 - If the source is too complex, choose the closest readable Chart.js representation instead of refusing.`
92
93 export const buildPptxChartRewriteUserPrompt = (request: PptxChartRewriteRequest): string => {
94 const chart = summarizeChartElement(request)
95 return `Rewrite this PPTX chart as a Chart.js config.
96
97 Output strict JSON only:
98 {
99 "config": {
100 "type": "line",
101 "data": { "labels": [], "datasets": [] },
102 "options": { "responsive": true, "maintainAspectRatio": false }
103 },
104 "warnings": []
105 }
106
107 Importer frame context that must be preserved by the caller, not rewritten by you:
108 {
109 "blockId": ${JSON.stringify(request.blockId)},
110 "canvasId": ${JSON.stringify(request.canvasId)},
111 "frameStyle": ${JSON.stringify(request.frameStyle)},
112 "animationAttrs": ${JSON.stringify(request.animationAttrs)}
113 }
114
115 PPTX chart element summary:
116 ${JSON.stringify(chart, null, 2)}`
117 }
118
119 const isRecord = (value: unknown): value is Record<string, unknown> =>
120 Boolean(value && typeof value === 'object' && !Array.isArray(value))
121
122 export const parsePptxChartRewriteAgentResponse = (
123 response: unknown
124 ): PptxChartRewriteResult | null => {
125 const text = extractModelText(response) || (typeof response === 'string' ? response : '')
126 const jsonText = extractJsonBlock(text).trim()
127 if (!jsonText) return null
128
129 const parsed = JSON.parse(jsonText) as Record<string, unknown>
130 const config = parsed.config
131 if (!isRecord(config) || typeof config.type !== 'string') return null
132 const data = config.data
133 if (!isRecord(data) || !Array.isArray(data.datasets)) return null
134
135 const options = isRecord(config.options) ? config.options : {}
136 config.options = {
137 ...options,
138 responsive: true,
139 maintainAspectRatio: false
140 }
141
142 const warnings = Array.isArray(parsed.warnings)
143 ? parsed.warnings.map((item) => String(item || '').trim()).filter(Boolean)
144 : []
145
146 return { config, warnings }
147 }
148
149 const rewritePptxChart = async (
150 options: PptxChartRewriteAgentOptions,
151 request: PptxChartRewriteRequest
152 ): Promise<PptxChartRewriteResult | null> => {
153 const docs = await readChartSkillDocs()
154 const model = resolveModel(
155 options.provider,
156 options.apiKey,
157 options.model,
158 options.baseUrl,
159 0.2,
160 options.maxTokens,
161 options.modelRuntime
162 )
163 const response = await model.invoke(
164 [
165 { role: 'system', content: buildPptxChartRewriteSystemPrompt(docs) },
166 { role: 'user', content: buildPptxChartRewriteUserPrompt(request) }
167 ],
168 { signal: AbortSignal.timeout(resolveModelTimeoutMs(options.modelTimeoutMs, 'document')) }
169 )
170 return parsePptxChartRewriteAgentResponse(response)
171 }
172
173 export const createPptxChartRewriteHandler = (
174 options: PptxChartRewriteAgentOptions
175 ): PptxChartRewriteHandler => {
176 let rewriteCount = 0
177 const maxRewrites = Math.max(0, options.maxRewrites ?? DEFAULT_MAX_REWRITES)
178 return async (request) => {
179 if (rewriteCount >= maxRewrites) return null
180 rewriteCount += 1
181 try {
182 const result = await rewritePptxChart(options, request)
183 log.info('[pptx:chartRewrite] completed', {
184 pageNumber: request.pageNumber,
185 blockId: request.blockId,
186 chartType: request.element.chartType,
187 success: Boolean(result)
188 })
189 return result
190 } catch (error) {
191 log.warn('[pptx:chartRewrite] failed', {
192 pageNumber: request.pageNumber,
193 blockId: request.blockId,
194 chartType: request.element.chartType,
195 message: error instanceof Error ? error.message : String(error)
196 })
197 return null
198 }
199 }
200 }
201
201 lines TYPESCRIPT