| 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 |