返回 oh-my-ppt
index.ts
1 import { HumanMessage } from '@langchain/core/messages'
2 import { isSupportedImageMimeType, normalizeImageMimeType } from '@shared/image-mime'
3 import { resolveModelTimeoutMs } from '@shared/model-timeout'
4 import log from 'electron-log/main.js'
5 import { extractModelText } from '../../model/result'
6 import { resolveModel } from '../../model/resolve'
7 import type { ModelRuntimeConfig } from '../../model/usage'
8
9 const combineAbortSignals = (signals: AbortSignal[]): { signal: AbortSignal; dispose: () => void } => {
10 if (signals.length === 1) return { signal: signals[0], dispose: () => undefined }
11
12 const controller = new AbortController()
13 const abort = (signal: AbortSignal): void => controller.abort(signal.reason)
14 const listeners = signals.map((signal) => {
15 const listener = (): void => abort(signal)
16 if (signal.aborted) listener()
17 else signal.addEventListener('abort', listener, { once: true })
18 return { signal, listener }
19 })
20 return {
21 signal: controller.signal,
22 dispose: () => {
23 for (const { signal, listener } of listeners) {
24 signal.removeEventListener('abort', listener)
25 }
26 }
27 }
28 }
29
30 export async function invokeVisionModelText(args: {
31 imageBase64: string
32 mimeType: string
33 prompt: string
34 provider: string
35 apiKey: string
36 model: string
37 baseUrl: string
38 maxTokens?: number
39 modelRuntime?: ModelRuntimeConfig
40 modelTimeoutMs: number
41 logTag: string
42 signal?: AbortSignal
43 }): Promise<string> {
44 const mimeType = normalizeImageMimeType(args.mimeType)
45 const imageBase64 = String(args.imageBase64 || '').trim()
46 if (!isSupportedImageMimeType(args.mimeType)) {
47 throw new Error(`不支持的图片格式:${mimeType || 'unknown'}`)
48 }
49 if (!imageBase64) {
50 throw new Error('图片数据为空')
51 }
52
53 const imageBytes = Buffer.byteLength(imageBase64, 'base64')
54 log.info(`[${args.logTag}] invoke vision model`, {
55 provider: args.provider,
56 model: args.model,
57 mimeType,
58 imageBytes
59 })
60
61 const model = resolveModel(
62 args.provider,
63 args.apiKey,
64 args.model,
65 args.baseUrl,
66 0.2,
67 args.maxTokens,
68 args.modelRuntime
69 )
70 const imageUrl = `data:${mimeType};base64,${imageBase64}`
71 const timeoutSignal = AbortSignal.timeout(resolveModelTimeoutMs(args.modelTimeoutMs, 'document'))
72 const combinedSignal = combineAbortSignals(
73 args.signal ? [args.signal, timeoutSignal] : [timeoutSignal]
74 )
75 try {
76 const result = await model.invoke(
77 [
78 new HumanMessage({
79 content: [
80 { type: 'text', text: args.prompt },
81 { type: 'image_url', image_url: { url: imageUrl } }
82 ]
83 })
84 ],
85 { signal: combinedSignal.signal }
86 )
87 return extractModelText(result)
88 } finally {
89 combinedSignal.dispose()
90 }
91 }
92
92 lines TYPESCRIPT