import { OpenAI } from 'openai'; import { createRequire } from 'module'; import config from '../config.js'; const require = createRequire(import.meta.url); // 预加载豆包模块(如果需要) // doubao 模块使用 CommonJS,需要通过 require 加载 let doubaoImgReg = null; function getDoubaoImgReg() { if (!doubaoImgReg) { doubaoImgReg = require('./doubao/img-reg.js'); } return doubaoImgReg; } // 注意:只有在使用非豆包模型时才需要 OpenAI 客户端 // 豆包模型使用自己的 API URL,不会使用 BASE_URL_IMG2TEXT let client = null; function getOpenAIClient() { if (!client) { client = new OpenAI({ apiKey: config.API_KEY, baseURL: config.BASE_URL_IMG2TEXT }); } return client; } /** * 检查模型是否为豆包模型 */ function isDoubaoModel(modelName) { if (!modelName) return false; const modelLower = modelName.toLowerCase(); return modelLower.includes('doubao') || modelLower.includes('豆包') || modelLower.includes('doubao-1.5') || modelLower.includes('ep-'); // 豆包的 endpoint 格式 } /** * 从 data URI 中提取 base64 数据 */ function extractBase64FromDataUri(dataUri) { // data:image/jpeg;base64,/9j/4AAQSkZJRg... const match = dataUri.match(/^data:([^;]+);base64,(.+)$/); if (match) { return { mimeType: match[1], base64: match[2] }; } // 如果不是 data URI,直接返回 return { mimeType: 'image/jpeg', base64: dataUri }; } async function img2text(prompt, imageUrl) { try { if (!prompt) { throw new Error('Missing prompt'); } if (!imageUrl) { throw new Error('Missing imageUrl'); } // 检查是否为豆包模型 if (isDoubaoModel(config.MODEL_IMG2TEXT)) { console.log(`📦 使用豆包模型: ${config.MODEL_IMG2TEXT}`); console.log(`⚠️ 注意:豆包模型使用自己的 API URL,不使用 BASE_URL_IMG2TEXT`); // 直接使用 require 调用豆包的 imgReg 函数 // 豆包的 API URL 在 doubao/doubao.js 中定义:https://ark.cn-beijing.volces.com/api/v3/responses const imgReg = getDoubaoImgReg(); // 从 imageUrl 中提取 base64 数据(img-reg.js 需要不含 data URI 前缀的 base64) const { base64, mimeType } = extractBase64FromDataUri(imageUrl); // 调用豆包的图片理解 API(使用豆包自己的 BASE_URL_DOUBAO,不会使用 BASE_URL_IMG2TEXT) const result = await imgReg(prompt, base64, null, mimeType, config.MODEL_IMG2TEXT); // 转换返回格式以匹配 OpenAI 格式 // 豆包的返回格式可能是: // 1. { success: true, data: { output: { text: "..." } } } // 2. { success: true, data: { output: { choices: [{ message: { content: "..." } }] } } } // 3. { success: true, data: { text: "..." } } // OpenAI 格式: { success: true, data: { choices: [{ message: { content: ... } }] } } if (result && result.data) { // 调试:打印原始返回数据(前500字符) const resultStr = JSON.stringify(result.data); console.log(`📦 豆包原始返回数据(前500字符): ${resultStr.substring(0, 500)}`); // 尝试多种方式提取响应文本 let content = ''; // 方式1: result.data.output.text if (result.data.output && typeof result.data.output.text === 'string') { content = result.data.output.text; } // 方式2: result.data.output.choices[0].message.content else if (result.data.output && result.data.output.choices && Array.isArray(result.data.output.choices)) { content = result.data.output.choices[0]?.message?.content || ''; } // 方式3: result.data.text else if (typeof result.data.text === 'string') { content = result.data.text; } // 方式4: result.data.response.text else if (result.data.response && typeof result.data.response.text === 'string') { content = result.data.response.text; } // 方式5: 递归查找包含文本的字段 else { const findTextInObject = (obj) => { if (typeof obj === 'string' && obj.length > 50) { return obj; // 如果找到较长的字符串,可能是内容 } if (typeof obj === 'object' && obj !== null) { for (const key in obj) { if (key === 'text' || key === 'content' || key === 'message') { const value = obj[key]; if (typeof value === 'string') return value; if (typeof value === 'object') { const found = findTextInObject(value); if (found) return found; } } } // 递归查找所有子对象 for (const key in obj) { if (typeof obj[key] === 'object') { const found = findTextInObject(obj[key]); if (found) return found; } } } return null; }; content = findTextInObject(result.data) || ''; } console.log(`📝 提取的文本内容长度: ${content.length} 字符`); // 转换为 OpenAI 兼容格式 return { success: true, data: { choices: [{ message: { content: content, role: 'assistant' } }] } }; } // 如果格式不匹配,记录并返回原始结果 console.warn('⚠️ 无法解析豆包返回格式,返回原始结果:', JSON.stringify(result).substring(0, 200)); return result; } // 使用 OpenAI 兼容的 API(非豆包模型,使用 BASE_URL_IMG2TEXT) const response = await getOpenAIClient().chat.completions.create({ model: config.MODEL_IMG2TEXT, messages: [ { role: 'user', content: [ { type: 'text', text: prompt }, { type: 'image_url', image_url: { url: imageUrl, detail: 'high' } } ] } ], max_tokens: 8000, // 增加token限制以生成更长的小说 stream: false }); return { success: true, data: response }; } catch (error) { throw error; } } export default img2text;