Files
2026-01-10 23:27:13 +08:00

990 lines
36 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* API服务
* 通过后端API代理调用第三方服务,保护API密钥安全
*/
import { logger } from '../utils/logger.js';
// 用于存储进行中的请求的AbortController
const runningRequests = new Map();
// ---基础路由推断逻辑---
// 自动获取当前插件的挂载点,消除硬编码路径
// 通过解析当前脚本的 URL,提取出 extensions/ 后的目录名
let apiBaseUrl = null;
function getDynamicApiBase() {
if (apiBaseUrl) return apiBaseUrl;
try {
const scriptUrl = import.meta.url;
const url = new URL(scriptUrl);
const pathParts = url.pathname.split('/');
// 策略1:寻找 /js/ 目录段,并取其前一个段作为插件名 (最通用)
const jsIdx = pathParts.indexOf('js');
if (jsIdx > 0) {
const nodeDir = pathParts[jsIdx - 1];
apiBaseUrl = `/${nodeDir}/api`;
}
// 策略2:回退到 extensions 关键词寻找 (ComfyUI 标准结构)
else {
const extIdx = pathParts.indexOf('extensions');
if (extIdx !== -1 && pathParts.length > extIdx + 1) {
const nodeDir = pathParts[extIdx + 1];
apiBaseUrl = `/${nodeDir}/api`;
} else {
// 策略3:硬编码兜底
apiBaseUrl = '/prompt-assistant/api';
}
}
} catch (e) {
apiBaseUrl = '/prompt-assistant/api';
}
return apiBaseUrl;
}
class APIService {
/**
* 获取动态 API 基础路径
* 暴露模块级的 getDynamicApiBase 函数供外部调用
*/
static getDynamicApiBase() {
return getDynamicApiBase();
}
/**
* 构建完整的API URL
*/
static getApiUrl(path) {
// 获取动态基础路由 (例如 /comfyui_prompt_assistant/api)
const baseApi = getDynamicApiBase();
// 确保 path 不含重复的前缀,且格式正确
let subPath = path.startsWith('/') ? path : `/${path}`;
// 如果 path 已经包含了 baseApi,则不再重复添加
const fullPath = subPath.startsWith(baseApi) ? subPath : `${baseApi}${subPath}`;
const url = `${window.location.origin}${fullPath}`;
// logger.debug(`构建API URL: ${url}`);
return url;
}
/**
* 解析形如 [{"id":0,"text":"..."}, ...] 的JSON数组
* 返回 Map<id, text>
*/
static _extractIndexedTranslations(text) {
const arr = APIService._extractJsonArray(text);
if (!Array.isArray(arr)) return null;
const map = new Map();
for (const item of arr) {
if (!item || typeof item !== 'object') return null;
if (!('id' in item) || !('text' in item)) return null;
map.set(Number(item.id), String(item.text ?? ''));
}
return map;
}
/**
* 结构化批量翻译(纯前端封装,单次LLM请求)
* 要求模型严格返回 JSON 数组,与输入 texts 一一对应
*/
static async llmBatchTranslate(texts, from = 'auto', to = 'zh', request_id = null) {
try {
if (!Array.isArray(texts) || texts.length === 0) {
throw new Error('待翻译文本数组不能为空');
}
// 构造结构化指令,使用索引,要求输出严格的 JSON 对象数组
const indexed = texts.map((t, i) => ({ id: i, text: t }));
// 强化提示词:明确禁止 Markdown 表格格式,禁止添加 '|' 前缀
const sysHint = `你是一个专业的翻译API。请将输入的 JSON 数组中的 text 字段内容从 ${from} 翻译为 ${to}。
规则:
1. 保持 JSON 结构不变,返回包含 id 和 text 的数组。
2. 严禁使用 Markdown 表格格式,严禁在译文前添加 '|' 符号。
3. 对于参数名、变量名(如 snake_case 格式),请尽可能将其翻译为中文含义(例如:pose_images -> 姿势图像),除非是专有名词(如 CLIP, VAE)。
4. 保持数组长度与输入一致。
5. 直接输出 JSON,不要包含 Markdown 代码块标记(如 \`\`\`json)。`;
const payload = { segments: indexed };
const prompt = [
sysHint,
'Input: ' + JSON.stringify(payload)
].join('\n');
// 复用单文本接口
const res = await this.llmTranslate(prompt, from, to, request_id);
if (!res || !res.success) {
return { success: false, error: res?.error || '批量翻译失败' };
}
const content = (res.data && (res.data.translated || res.data.expanded || res.data.content)) || res.translated || res.content || '';
// 解析为索引映射
let mapped = APIService._extractIndexedTranslations(content);
if (!mapped) {
// 回退:尝试解析为纯字符串数组并顺序对齐
const arr = APIService._extractJsonArray(content);
if (Array.isArray(arr) && arr.length === texts.length) {
mapped = new Map(arr.map((v, i) => [i, v]));
}
}
if (!mapped) {
return { success: false, error: '解析批量译文失败:未检测到有效JSON' };
}
// 如果缺项,针对缺失索引做单次补救调用,避免整次失败
const translations = new Array(texts.length).fill("");
const missingIdx = [];
for (let i = 0; i < texts.length; i++) {
if (mapped.has(i)) {
translations[i] = mapped.get(i);
} else {
missingIdx.push(i);
}
}
if (missingIdx.length > 0) {
for (const i of missingIdx) {
const single = await this.llmTranslate(texts[i], from, to, request_id);
if (single && single.success) {
translations[i] = (single.data && single.data.translated) || '';
} else {
translations[i] = '';
}
}
}
return { success: true, data: { translations } };
} catch (error) {
return { success: false, error: error.message };
}
}
/**
* 并行分块批量翻译(优化版本)
* 将文本数组分块后并行发起多个翻译请求,显著提升翻译速度
*
* @param {string[]} texts - 待翻译的文本数组
* @param {string} from - 源语言
* @param {string} to - 目标语言
* @param {Object} options - 配置选项
* @param {number} options.chunkSize - 每块包含的文本数量(默认5)
* @param {number} options.concurrency - 最大并发数(默认3)
* @param {Function} options.onProgress - 进度回调 (completedChunks, totalChunks)
* @returns {Promise<{success: boolean, data?: {translations: string[]}, error?: string}>}
*/
static async llmParallelBatchTranslate(texts, from = 'auto', to = 'zh', options = {}) {
const { chunkSize = 5, concurrency = 3, onProgress = null } = options;
try {
if (!Array.isArray(texts) || texts.length === 0) {
throw new Error('待翻译文本数组不能为空');
}
// 1. 分块
const chunks = [];
for (let i = 0; i < texts.length; i += chunkSize) {
chunks.push({
startIndex: i,
texts: texts.slice(i, i + chunkSize)
});
}
logger.log(`[APIService] 并行分块翻译 | 总文本数:${texts.length} | 分块数:${chunks.length} | 每块:${chunkSize} | 并发:${concurrency}`);
// 2. 创建结果数组
const allTranslations = new Array(texts.length).fill('');
let completedChunks = 0;
let hasError = false;
let lastError = null;
// 3. 并发控制函数
const translateChunk = async (chunk) => {
try {
const result = await this.llmBatchTranslate(chunk.texts, from, to);
if (result.success && result.data && result.data.translations) {
// 将翻译结果填充到对应位置
result.data.translations.forEach((translation, idx) => {
allTranslations[chunk.startIndex + idx] = translation || '';
});
} else {
// 单块失败,记录但不中断
hasError = true;
lastError = result.error || '翻译失败';
logger.warn(`[APIService] 分块翻译失败 | 起始索引:${chunk.startIndex} | 错误:${lastError}`);
}
} catch (err) {
hasError = true;
lastError = err.message;
logger.error(`[APIService] 分块翻译异常 | 起始索引:${chunk.startIndex} | 错误:${err.message}`);
} finally {
completedChunks++;
if (onProgress) {
onProgress(completedChunks, chunks.length);
}
}
};
// 4. 分批并发执行(控制最大并发数)
for (let i = 0; i < chunks.length; i += concurrency) {
const batch = chunks.slice(i, i + concurrency);
await Promise.all(batch.map(chunk => translateChunk(chunk)));
}
// 5. 检查结果
const successCount = allTranslations.filter(t => t && t.trim()).length;
logger.log(`[APIService] 并行翻译完成 | 成功:${successCount}/${texts.length}`);
// 即使部分失败也返回成功(有翻译结果)
if (successCount === 0 && texts.length > 0) {
return { success: false, error: lastError || '所有文本翻译失败' };
}
return { success: true, data: { translations: allTranslations } };
} catch (error) {
logger.error(`[APIService] 并行批量翻译失败: ${error.message}`);
return { success: false, error: error.message };
}
}
/**
* 从字符串中提取并解析首个JSON数组
*/
static _extractJsonArray(text) {
if (!text) return null;
try {
// 快速路径:整段就是JSON数组
if (text.trim().startsWith('[')) {
return JSON.parse(text.trim());
}
} catch (_) { /* ignore */ }
// 兼容模型添加的前后缀:寻找第一个 '[' 与匹配的 ']'
const first = text.indexOf('[');
const last = text.lastIndexOf(']');
if (first === -1 || last === -1 || last <= first) return null;
const candidate = text.slice(first, last + 1);
try {
return JSON.parse(candidate);
} catch (e) {
// 尝试修正全角引号
const normalized = candidate.replace(/[“”]/g, '"');
try { return JSON.parse(normalized); } catch { return null; }
}
}
/**
* 生成唯一请求ID
*/
/**
* 生成唯一请求ID
* 格式: 请求类型_服务类型(可选)_NodeID_四位时间戳
*/
static generateRequestId(type, serviceType = null, nodeId = '0') {
const timestamp = Math.floor(Date.now() / 1000).toString().slice(-4);
const parts = [type];
if (serviceType) {
parts.push(serviceType);
}
parts.push(nodeId);
parts.push(timestamp);
return parts.join('_');
}
/**
* 取消一个正在进行的请求
*/
static async cancelRequest(requestId) {
if (!requestId) return { success: false, error: "缺少requestId" };
const controller = runningRequests.get(requestId);
if (controller) {
// 1. 中止前端的fetch请求
controller.abort();
runningRequests.delete(requestId);
logger.debug(`前端请求已中止 | ID: ${requestId}`);
}
// 2. 通知后端取消任务
try {
const apiUrl = this.getApiUrl('request/cancel');
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ request_id: requestId })
});
const result = await response.json();
logger.debug(`后端任务取消请求已发送 | ID: ${requestId} | 结果: ${JSON.stringify(result)}`);
return result;
} catch (error) {
logger.error(`后端任务取消请求失败 | ID: ${requestId} | 错误: ${error.message}`);
return { success: false, error: error.message };
}
}
/**
* 百度翻译API
*/
static async baiduTranslate(text, from = 'auto', to = 'zh', request_id = null, is_auto = false) {
// 生成请求ID
// 生成请求ID
if (!request_id) {
request_id = this.generateRequestId('trans', 'baidu');
}
const controller = new AbortController();
const signal = controller.signal;
runningRequests.set(request_id, controller);
try {
if (!text || text.trim() === '') {
throw new Error('待翻译文本不能为空');
}
// 获取API URL
const apiUrl = this.getApiUrl('baidu/translate');
// 调用后端API
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
text,
from,
to,
request_id,
is_auto
}),
signal // 传递signal
});
const result = await response.json();
return result;
} catch (error) {
if (error.name === 'AbortError') {
logger.debug(`百度翻译请求被用户中止 | ID: ${request_id}`);
return { success: false, error: '请求已取消', cancelled: true };
}
return {
success: false,
error: error.message
};
} finally {
// 请求完成后从Map中移除
if (runningRequests.has(request_id)) {
runningRequests.delete(request_id);
}
}
}
/**
* 批量翻译
*/
static async batchBaiduTranslate(texts, from = 'auto', to = 'zh') {
try {
if (!Array.isArray(texts) || texts.length === 0) {
throw new Error('待翻译文本数组不能为空');
}
// 串行处理每个文本的翻译
const results = [];
for (const text of texts) {
const result = await this.baiduTranslate(text, from, to);
results.push(result);
}
return results;
} catch (error) {
logger.error(`批量翻译 | 结果:失败 | 错误:${error.message}`);
return [];
}
}
/**
* LLM扩写提示词
*/
static async llmExpandPrompt(prompt, request_id = null) {
// 生成请求ID
// 生成请求ID
if (!request_id) {
request_id = this.generateRequestId('exp');
}
const controller = new AbortController();
const signal = controller.signal;
runningRequests.set(request_id, controller);
try {
if (!prompt || prompt.trim() === '') {
throw new Error('请输入要优化的提示词');
}
logger.debug(`发起LLM提示词优化请求 | 请求ID:${request_id} | 原文:${prompt}`);
// 调用后端API
const apiUrl = this.getApiUrl('llm/expand');
logger.debug('LLM提示词优化API URL:', apiUrl);
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
prompt,
request_id
}),
signal // 传递signal
});
const result = await response.json();
logger.debug(`LLM提示词优化请求成功 | 请求ID:${request_id} | 结果:${JSON.stringify(result)}`);
return result;
} catch (error) {
if (error.name === 'AbortError') {
logger.debug(`LLM提示词优化请求被用户中止 | ID: ${request_id}`);
return { success: false, error: '请求已取消', cancelled: true };
}
logger.error(`LLM提示词优化请求失败 | 请求ID:${request_id || 'unknown'} | 错误:${error.message}`);
return {
success: false,
error: error.message
};
} finally {
// 请求完成后从Map中移除
if (runningRequests.has(request_id)) {
runningRequests.delete(request_id);
}
}
}
/**
* LLM翻译文本
*/
static async llmTranslate(text, from = 'auto', to = 'zh', request_id = null, is_auto = false) {
// 生成请求ID
// 生成请求ID
if (!request_id) {
request_id = this.generateRequestId('trans', 'llm');
}
const controller = new AbortController();
const signal = controller.signal;
runningRequests.set(request_id, controller);
try {
if (!text || text.trim() === '') {
throw new Error('请输入要翻译的内容');
}
// 调用后端API
const apiUrl = this.getApiUrl('llm/translate');
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
text,
from,
to,
request_id,
is_auto
}),
signal // 传递signal
});
const result = await response.json();
return result;
} catch (error) {
if (error.name === 'AbortError') {
logger.debug(`LLM翻译请求被用户中止 | ID: ${request_id}`);
return { success: false, error: '请求已取消', cancelled: true };
}
return {
success: false,
error: error.message
};
} finally {
// 请求完成后从Map中移除
if (runningRequests.has(request_id)) {
runningRequests.delete(request_id);
}
}
}
/**
* 调用视觉模型分析图像
*/
static async llmAnalyzeImage(imageData, prompt, request_id = null) {
// 生成请求ID
// 生成请求ID
if (!request_id) {
request_id = this.generateRequestId('icap');
}
const controller = new AbortController();
const signal = controller.signal;
runningRequests.set(request_id, controller);
try {
if (!imageData) {
throw new Error('未找到有效的图像');
}
logger.debug(`发起视觉分析请求 | 请求ID:${request_id}`);
// 构建API URL
const apiUrl = this.getApiUrl('vlm/analyze');
logger.debug('视觉分析API URL:', apiUrl);
// 构建请求数据
const requestData = {
image: imageData,
prompt: prompt, // 添加prompt
request_id: request_id
};
// 发送请求
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData),
signal // 传递signal
});
// 解析响应
const result = await response.json();
logger.debug(`视觉分析请求完成 | 请求ID:${request_id}`);
return result;
} catch (error) {
if (error.name === 'AbortError') {
logger.debug(`视觉分析请求被用户中止 | ID: ${request_id}`);
return { success: false, error: '请求已取消', cancelled: true };
}
logger.error(`视觉分析请求失败 | 请求ID:${request_id || 'unknown'} | 错误:${error.message}`);
return {
success: false,
error: error.message || '请求失败'
};
} finally {
// 请求完成后从Map中移除
if (runningRequests.has(request_id)) {
runningRequests.delete(request_id);
}
}
}
// ---流式输出API方法(SSE)---
/**
* 流式视觉分析图像
* 使用 SSE 逐 token 接收分析结果
* @param {string} imageData - Base64 编码的图像数据
* @param {string} prompt - 分析提示词
* @param {string} request_id - 请求ID
* @param {Function} onChunk - 接收每个 chunk 的回调函数
* @returns {Promise<Object>} - 完整的分析结果
*/
static async llmAnalyzeImageStream(imageData, prompt, request_id = null, onChunk = null) {
if (!request_id) {
request_id = this.generateRequestId('icap');
}
const controller = new AbortController();
const signal = controller.signal;
runningRequests.set(request_id, controller);
try {
if (!imageData) {
throw new Error('未找到有效的图像');
}
logger.debug(`发起流式视觉分析请求 | 请求ID:${request_id}`);
const apiUrl = this.getApiUrl('vlm/analyze/stream');
const requestData = {
image: imageData,
prompt: prompt,
request_id: request_id
};
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestData),
signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let finalResult = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.chunk && onChunk) {
onChunk(data.chunk);
}
if (data.done) {
finalResult = data.result;
}
if (data.error) {
throw new Error(data.error);
}
} catch (parseError) {
if (parseError.message !== 'Unexpected end of JSON input') {
logger.warn(`解析 SSE 数据失败: ${parseError.message}`);
}
}
}
}
}
logger.debug(`流式视觉分析请求完成 | 请求ID:${request_id}`);
return finalResult;
} catch (error) {
if (error.name === 'AbortError') {
logger.debug(`流式视觉分析请求被用户中止 | ID: ${request_id}`);
return { success: false, error: '请求已取消', cancelled: true };
}
logger.error(`流式视觉分析请求失败 | 请求ID:${request_id || 'unknown'} | 错误:${error.message}`);
return { success: false, error: error.message || '请求失败' };
} finally {
if (runningRequests.has(request_id)) {
runningRequests.delete(request_id);
}
}
}
/**
* 流式LLM扩写提示词
* 使用 SSE 逐 token 接收扩写结果
* @param {string} prompt - 要扩写的提示词
* @param {string} request_id - 请求ID
* @param {Function} onChunk - 接收每个 chunk 的回调函数
* @returns {Promise<Object>} - 完整的扩写结果
*/
static async llmExpandPromptStream(prompt, request_id = null, onChunk = null) {
if (!request_id) {
request_id = this.generateRequestId('exp');
}
const controller = new AbortController();
const signal = controller.signal;
runningRequests.set(request_id, controller);
try {
if (!prompt || prompt.trim() === '') {
throw new Error('请输入要优化的提示词');
}
logger.debug(`发起流式LLM提示词优化请求 | 请求ID:${request_id}`);
const apiUrl = this.getApiUrl('llm/expand/stream');
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, request_id }),
signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let finalResult = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.chunk && onChunk) {
onChunk(data.chunk);
}
if (data.done) {
finalResult = data.result;
}
if (data.error) {
throw new Error(data.error);
}
} catch (parseError) {
if (parseError.message !== 'Unexpected end of JSON input') {
logger.warn(`解析 SSE 数据失败: ${parseError.message}`);
}
}
}
}
}
logger.debug(`流式LLM提示词优化请求完成 | 请求ID:${request_id}`);
return finalResult;
} catch (error) {
if (error.name === 'AbortError') {
logger.debug(`流式LLM提示词优化请求被用户中止 | ID: ${request_id}`);
return { success: false, error: '请求已取消', cancelled: true };
}
logger.error(`流式LLM提示词优化请求失败 | 请求ID:${request_id || 'unknown'} | 错误:${error.message}`);
return { success: false, error: error.message };
} finally {
if (runningRequests.has(request_id)) {
runningRequests.delete(request_id);
}
}
}
/**
* 流式LLM翻译
* 使用 SSE 逐 token 接收翻译结果
* 注意:仅支持LLM翻译服务,百度翻译不支持流式
* @param {string} text - 要翻译的文本
* @param {string} fromLang - 源语言
* @param {string} toLang - 目标语言
* @param {string} request_id - 请求ID
* @param {Function} onChunk - 接收每个 chunk 的回调函数
* @returns {Promise<Object>} - 完整的翻译结果
*/
static async llmTranslateStream(text, fromLang, toLang, request_id = null, onChunk = null) {
if (!request_id) {
request_id = this.generateRequestId('trans');
}
const controller = new AbortController();
const signal = controller.signal;
runningRequests.set(request_id, controller);
try {
if (!text || text.trim() === '') {
throw new Error('请输入要翻译的内容');
}
logger.debug(`发起流式LLM翻译请求 | 请求ID:${request_id} | ${fromLang}→${toLang}`);
const apiUrl = this.getApiUrl('llm/translate/stream');
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text,
from: fromLang,
to: toLang,
request_id
}),
signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let finalResult = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.chunk && onChunk) {
onChunk(data.chunk);
}
if (data.done) {
finalResult = data.result;
}
if (data.error) {
throw new Error(data.error);
}
} catch (parseError) {
if (parseError.message !== 'Unexpected end of JSON input') {
logger.warn(`解析 SSE 数据失败: ${parseError.message}`);
}
}
}
}
}
logger.debug(`流式LLM翻译请求完成 | 请求ID:${request_id}`);
return finalResult;
} catch (error) {
if (error.name === 'AbortError') {
logger.debug(`流式LLM翻译请求被用户中止 | ID: ${request_id}`);
return { success: false, error: '请求已取消', cancelled: true };
}
logger.error(`流式LLM翻译请求失败 | 请求ID:${request_id || 'unknown'} | 错误:${error.message}`);
return { success: false, error: error.message };
} finally {
if (runningRequests.has(request_id)) {
runningRequests.delete(request_id);
}
}
}
/**
* 将图像转换为Base64
*/
static async imageToBase64(img) {
return new Promise((resolve, reject) => {
try {
if (!img) {
reject('无效的图像');
return;
}
// 如果已经是base64字符串,直接返回
if (typeof img === 'string' && img.startsWith('data:image')) {
resolve(img);
return;
}
// 如果是Blob对象
if (img instanceof Blob) {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(img);
return;
}
// 如果是URL
if (typeof img === 'string' && (img.startsWith('http') || img.startsWith('/'))) {
const image = new Image();
image.crossOrigin = 'Anonymous';
image.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
resolve(canvas.toDataURL('image/jpeg'));
};
image.onerror = () => reject('图像加载失败');
image.src = img;
return;
}
// 处理ComfyUI图像对象
if (img && typeof img === 'object' && img.src) {
// 如果图像对象有src属性,使用它
const image = new Image();
image.crossOrigin = 'Anonymous';
image.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
resolve(canvas.toDataURL('image/jpeg'));
};
image.onerror = (e) => {
console.error('图像加载失败:', e);
reject('图像加载失败');
};
image.src = img.src;
return;
}
// 处理HTMLImageElement或类似的对象
if (img && (img instanceof HTMLImageElement || (img.width && img.height && img.complete))) {
try {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
resolve(canvas.toDataURL('image/jpeg'));
return;
} catch (e) {
console.warn('使用canvas转换图像失败:', e);
// 继续尝试其他方法
}
}
// 处理ComfyUI的特殊格式 (dataURL缓存在node中)
if (img && img.dataURL) {
resolve(img.dataURL);
return;
}
// 处理ComfyUI特殊的图像数据格式
if (img && img.data && img.width && img.height) {
try {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
const imageData = new ImageData(
new Uint8ClampedArray(img.data.buffer || img.data),
img.width,
img.height
);
ctx.putImageData(imageData, 0, 0);
resolve(canvas.toDataURL('image/jpeg'));
return;
} catch (e) {
console.error('处理图像数据失败:', e);
}
}
console.error('不支持的图像格式', img);
reject('不支持的图像格式');
} catch (error) {
console.error('转换图像出错:', error);
reject(error);
}
});
}
}
export { APIService };