/** * 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 */ 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} - 完整的分析结果 */ 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} - 完整的扩写结果 */ 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} - 完整的翻译结果 */ 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 };