返回 oh-my-ppt
image.ts
根目录 / src / main / styles / import / image.ts
1 import { buildStyleImageImportPrompt } from '../../agent-runtime/prompt'
2 import { parseStyleImportResponse, retryFixJson } from './pptx'
3 import type { StyleParseResult } from './file'
4 import { invokeVisionModelText } from '../../agent-runtime/provider/vision'
5 import type { ModelRuntimeConfig } from '../../agent-runtime/model'
6 import { isSupportedImageMimeType, normalizeImageMimeType } from '@shared/image-mime'
7
8 export async function parseStyleImage(args: {
9 imageBase64: string
10 mimeType: string
11 provider: string
12 apiKey: string
13 model: string
14 baseUrl: string
15 maxTokens?: number
16 modelRuntime?: ModelRuntimeConfig
17 modelTimeoutMs: number
18 }): Promise<StyleParseResult> {
19 const mimeType = normalizeImageMimeType(args.mimeType)
20 const imageBase64 = String(args.imageBase64 || '').trim()
21 if (!isSupportedImageMimeType(args.mimeType)) {
22 throw new Error(`不支持的图片格式:${mimeType || 'unknown'}`)
23 }
24 if (!imageBase64) {
25 throw new Error('图片数据为空')
26 }
27
28 const prompt = buildStyleImageImportPrompt()
29
30 let responseText = ''
31 try {
32 responseText = await invokeVisionModelText({
33 ...args,
34 mimeType,
35 imageBase64,
36 prompt,
37 logTag: 'styles:parseImage'
38 })
39 } catch (error) {
40 if (isImageUnsupportedError(error)) {
41 throw new Error('当前模型不支持图片解析,请在设置中切换到支持多模态的模型')
42 }
43 throw error
44 }
45
46 const parsed = await parseStyleImageResponseWithRepairs(responseText, args)
47 assertImageWasRead(`${parsed.label}\n${parsed.description}\n${parsed.styleSkill}`)
48 return parsed
49 }
50
51 async function parseStyleImageResponseWithRepairs(
52 responseText: string,
53 args: {
54 provider: string
55 apiKey: string
56 model: string
57 baseUrl: string
58 maxTokens?: number
59 modelRuntime?: ModelRuntimeConfig
60 modelTimeoutMs: number
61 }
62 ): Promise<StyleParseResult> {
63 let candidate = responseText
64 const maxRepairAttempts = 2
65 for (let repairAttempt = 0; repairAttempt <= maxRepairAttempts; repairAttempt += 1) {
66 try {
67 return parseStyleImportResponse(candidate)
68 } catch (parseError) {
69 if (repairAttempt >= maxRepairAttempts) throw parseError
70 const reason = parseError instanceof Error ? parseError.message : String(parseError)
71 candidate = await retryFixJson({
72 provider: args.provider,
73 apiKey: args.apiKey,
74 model: args.model,
75 baseUrl: args.baseUrl,
76 modelRuntime: args.modelRuntime,
77 modelTimeoutMs: args.modelTimeoutMs,
78 brokenResponse: candidate,
79 parseError: reason
80 })
81 }
82 }
83 throw new Error('LLM 返回格式异常:JSON 修复失败')
84 }
85
86 export function assertImageWasRead(text: string): void {
87 const normalized = text.toLowerCase()
88 const missingImagePatterns = [
89 /未提供图片/,
90 /未上传图片/,
91 /未发现可分析的图片/,
92 /未检测到图片/,
93 /没有图片/,
94 /无法完成图片分析/,
95 /无法分析图片/,
96 /图片文件未上传/,
97 /no image/,
98 /image (?:was )?not (?:provided|uploaded|attached)/,
99 /cannot (?:analyze|inspect|see|view) (?:the )?image/,
100 /unable to (?:analyze|inspect|see|view) (?:the )?image/
101 ]
102 if (missingImagePatterns.some((pattern) => pattern.test(normalized))) {
103 throw new Error('当前模型未能读取图片,请在设置中切换到支持多模态的模型后重试')
104 }
105 }
106
107 export function isImageUnsupportedError(error: unknown): boolean {
108 const message = error instanceof Error ? error.message : String(error || '')
109 const normalized = message.toLowerCase()
110 return [
111 /invalid_image/i,
112 /image not supported/i,
113 /does not support images/i,
114 /unsupported content type/i,
115 /does not support (?:multimodal|vision)/i,
116 /unsupported.*(?:multimodal|vision)/i,
117 /(?:multimodal|vision).*not (?:supported|available)/i
118 ].some((pattern) => pattern.test(normalized))
119 }
120
120 lines TYPESCRIPT