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