From 56ae965b73397142c54a00e86a802426a9034d41 Mon Sep 17 00:00:00 2001 From: shiyue <935732994@qq.com> Date: Thu, 5 Feb 2026 19:28:49 +0800 Subject: [PATCH] =?UTF-8?q?1.=20HTML=20=E8=AE=BE=E7=BD=AE=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=20(index.html:55-63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 API格式 下拉选择框: - raw - 原始API格式(使用 /v1/images/generations 端点) - openai - OpenAI格式(使用 /v1/chat/completions 端点) 2. JavaScript 核心逻辑 (script.js) 设置字段 (script.js:10) - currentSettings 新增 apiFormat: 'raw' 字段 API 服务 (script.js:685-916) - testConnection: 根据 API 格式选择 /models 或 /v1/models 端点 - generateImage: - 原始格式: 使用 prompt 请求体,端点 /v1/images/generations - OpenAI格式: 使用 messages 请求体,端点 /v1/chat/completions - extractImagesFromContent: 新增方法,支持从 Chat 响应中提取图像: - ✅ Markdown 图片语法 ![alt](url) - ✅ Base64 图像数据 - ✅ 通用图像 URL(放宽限制) - ✅ JSON 格式内容(包括代码块) - ✅ 多模态内容数组 设置持久化 (script.js:1591, 1771, 1909) - loadSettings / saveSettings 支持 apiFormat - 自动保存监听器包含 apiFormat 连接测试 (script.js:2027, 2037) - app.testConnection 传递 apiFormat 参数 --- index.html | 9 +++ script.js | 211 +++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 182 insertions(+), 38 deletions(-) diff --git a/index.html b/index.html index be9f169..0156b1a 100644 --- a/index.html +++ b/index.html @@ -52,6 +52,15 @@ +
+
+ + +
+
diff --git a/script.js b/script.js index 7f33c19..a57c0ea 100644 --- a/script.js +++ b/script.js @@ -7,6 +7,7 @@ let generatedImages = []; let currentSettings = { apiKey: '', baseUrl: '', + apiFormat: 'raw', // 'raw' 原始API格式, 'openai' OpenAI Chat格式 model: 'grok-imagine-1.0', timeout: 600, proxy: '', @@ -681,9 +682,12 @@ const indexedDBStorage = { // API服务 const apiService = { // 测试连接 - testConnection: async function(apiKey, baseUrl) { + testConnection: async function(apiKey, baseUrl, apiFormat) { try { - const response = await fetch(`${baseUrl}/models`, { + const normalizedBaseUrl = baseUrl.replace(/\/+$/, ''); + // 根据 API 格式选择不同的测试端点 + const modelsEndpoint = apiFormat === 'openai' ? '/v1/models' : '/models'; + const response = await fetch(`${normalizedBaseUrl}${modelsEndpoint}`, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' @@ -707,19 +711,41 @@ const apiService = { console.warn('当前 API 不支持图像编辑,将忽略上传的图像'); } - // 构建请求 payload(符合 OpenAI 图像生成 API 格式) - const payload = { - model: settings.model, - prompt: message, - n: settings.n || 1, - response_format: settings.responseFormat || 'b64_json' - }; + const apiFormat = settings.apiFormat || 'raw'; + const normalizedBaseUrl = settings.baseUrl.replace(/\/+$/, ''); + + // 根据 API 格式构建不同的请求 + let endpoint, payload; + + if (apiFormat === 'openai') { + // OpenAI Chat Completions 格式 + endpoint = `${normalizedBaseUrl}/v1/chat/completions`; + payload = { + model: settings.model, + messages: [ + { + role: 'user', + content: message + } + ], + n: settings.n || 1 + }; + } else { + // 原始 API 格式 (图像生成端点) + endpoint = `${normalizedBaseUrl}/v1/images/generations`; + payload = { + model: settings.model, + prompt: message, + n: settings.n || 1, + response_format: settings.responseFormat || 'b64_json' + }; + } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), settings.timeout * 1000); try { - const response = await fetch(`${settings.baseUrl}/v1/images/generations`, { + const response = await fetch(endpoint, { method: 'POST', headers: { 'Authorization': `Bearer ${settings.apiKey}`, @@ -738,32 +764,47 @@ const apiService = { const data = await response.json(); - // 解析响应(OpenAI 图像生成 API 格式) - const images = []; + // 根据 API 格式解析响应 + const resultImages = []; let hasError = false; - if (data.data && Array.isArray(data.data)) { - data.data.forEach(item => { - if (item.b64_json) { - // 检测 API 返回的错误标记 - if (item.b64_json === 'error' || item.b64_json === 'Error') { - hasError = true; - return; + if (apiFormat === 'openai') { + // 解析 OpenAI Chat Completions 响应 + if (data.choices && Array.isArray(data.choices)) { + data.choices.forEach(choice => { + const content = choice.message?.content; + if (content) { + // 尝试从 content 中提取图像 + const extractedImages = this.extractImagesFromContent(content); + resultImages.push(...extractedImages); } - // base64 格式响应 - const base64Data = `data:image/png;base64,${item.b64_json}`; - const blobUrl = utils.base64ToBlobUrl(base64Data); - images.push(blobUrl); - } else if (item.url) { - // 检测 URL 格式的错误标记 - if (item.url === 'error' || item.url === 'Error') { - hasError = true; - return; + }); + } + } else { + // 解析原始 API 响应(OpenAI 图像生成 API 格式) + if (data.data && Array.isArray(data.data)) { + data.data.forEach(item => { + if (item.b64_json) { + // 检测 API 返回的错误标记 + if (item.b64_json === 'error' || item.b64_json === 'Error') { + hasError = true; + return; + } + // base64 格式响应 + const base64Data = `data:image/png;base64,${item.b64_json}`; + const blobUrl = utils.base64ToBlobUrl(base64Data); + resultImages.push(blobUrl); + } else if (item.url) { + // 检测 URL 格式的错误标记 + if (item.url === 'error' || item.url === 'Error') { + hasError = true; + return; + } + // URL 格式响应 + resultImages.push(item.url); } - // URL 格式响应 - images.push(item.url); - } - }); + }); + } } // 如果检测到错误标记,抛出错误以触发重试 @@ -773,14 +814,105 @@ const apiService = { return { success: true, - content: `已生成 ${images.length} 张图像`, - images: images, + content: `已生成 ${resultImages.length} 张图像`, + images: resultImages, usage: data.usage || null }; } catch (error) { clearTimeout(timeoutId); throw error; } + }, + + // 从 OpenAI Chat 响应内容中提取图像 + extractImagesFromContent: function(content) { + const images = []; + + // 如果 content 是字符串,尝试解析 + if (typeof content === 'string') { + // 优先匹配 Markdown 图片语法 ![alt](url) + const markdownImageRegex = /!\[[^\]]*\]\(([^)]+)\)/g; + let mdMatch; + while ((mdMatch = markdownImageRegex.exec(content)) !== null) { + const url = mdMatch[1].trim(); + if (url && !images.includes(url)) { + images.push(url); + } + } + + // 尝试匹配 base64 图像数据 + const base64Regex = /data:image\/[a-zA-Z]+;base64,[A-Za-z0-9+/=]+/g; + const base64Matches = content.match(base64Regex); + if (base64Matches) { + base64Matches.forEach(match => { + if (!images.includes(match)) { + const blobUrl = utils.base64ToBlobUrl(match); + images.push(blobUrl); + } + }); + } + + // 尝试匹配通用图像 URL(放宽限制,支持无扩展名的签名URL) + const urlRegex = /https?:\/\/[^\s"'<>\)]+/gi; + const urlMatches = content.match(urlRegex); + if (urlMatches) { + urlMatches.forEach(url => { + // 清理 URL 末尾可能的标点符号 + const cleanUrl = url.replace(/[.,;:!?]+$/, ''); + // 检查是否像图像 URL(包含常见图像关键词或扩展名) + const isImageUrl = /\.(png|jpg|jpeg|gif|webp|bmp|svg)(\?|$)/i.test(cleanUrl) || + /image|img|photo|picture|generated|upload/i.test(cleanUrl); + if (isImageUrl && !images.includes(cleanUrl)) { + images.push(cleanUrl); + } + }); + } + + // 尝试解析 JSON 格式的内容(包括 Markdown 代码块中的 JSON) + let jsonStr = content; + // 尝试提取 Markdown 代码块中的 JSON + const codeBlockMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/); + if (codeBlockMatch) { + jsonStr = codeBlockMatch[1].trim(); + } + try { + const jsonContent = JSON.parse(jsonStr); + if (jsonContent.url && !images.includes(jsonContent.url)) { + images.push(jsonContent.url); + } + if (jsonContent.b64_json) { + const base64Data = `data:image/png;base64,${jsonContent.b64_json}`; + const blobUrl = utils.base64ToBlobUrl(base64Data); + images.push(blobUrl); + } + if (Array.isArray(jsonContent.data)) { + jsonContent.data.forEach(item => { + if (item.url && !images.includes(item.url)) images.push(item.url); + if (item.b64_json) { + const base64Data = `data:image/png;base64,${item.b64_json}`; + const blobUrl = utils.base64ToBlobUrl(base64Data); + images.push(blobUrl); + } + }); + } + } catch (e) { + // 不是 JSON 格式,忽略 + } + } else if (Array.isArray(content)) { + // 处理多模态内容数组 + content.forEach(part => { + if (part.type === 'image_url' && part.image_url?.url) { + images.push(part.image_url.url); + } + // 处理 text 类型中可能包含的图像 + if (part.type === 'text' && part.text) { + const textImages = this.extractImagesFromContent(part.text); + images.push(...textImages); + } + }); + } + + return images; } }; @@ -1587,6 +1719,7 @@ const uiController = { // 更新UI document.getElementById('apiKey').value = currentSettings.apiKey; document.getElementById('baseUrl').value = currentSettings.baseUrl; + document.getElementById('apiFormat').value = currentSettings.apiFormat || 'raw'; document.getElementById('model').value = currentSettings.model; document.getElementById('timeout').value = currentSettings.timeout; document.getElementById('proxy').value = currentSettings.proxy; @@ -1766,6 +1899,7 @@ const uiController = { try { currentSettings.apiKey = document.getElementById('apiKey').value; currentSettings.baseUrl = document.getElementById('baseUrl').value; + currentSettings.apiFormat = document.getElementById('apiFormat').value; currentSettings.model = document.getElementById('model').value; currentSettings.timeout = parseInt(document.getElementById('timeout').value); currentSettings.proxy = document.getElementById('proxy').value; @@ -1903,7 +2037,7 @@ const app = { } // 设置变化时自动保存 - const inputs = ['apiKey', 'baseUrl', 'model', 'timeout', 'proxy']; + const inputs = ['apiKey', 'baseUrl', 'apiFormat', 'model', 'timeout', 'proxy']; inputs.forEach(id => { document.getElementById(id).addEventListener('change', utils.debounce(() => { uiController.saveSettings(); @@ -1920,16 +2054,17 @@ const app = { testConnection: async function() { const apiKey = document.getElementById('apiKey').value; const baseUrl = document.getElementById('baseUrl').value; - + const apiFormat = document.getElementById('apiFormat').value; + if (!apiKey) { utils.showNotification(ERROR_MESSAGES.NO_API_KEY, 'warning'); return; } uiController.showLoading(true); - + try { - const result = await apiService.testConnection(apiKey, baseUrl); + const result = await apiService.testConnection(apiKey, baseUrl, apiFormat); if (result.success) { uiController.updateConnectionStatus(true);