1. HTML 设置面板 (index.html:55-63)

新增 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 参数
This commit is contained in:
2026-02-05 19:28:49 +08:00
parent d6e43d5324
commit 56ae965b73
2 changed files with 182 additions and 38 deletions

View File

@@ -52,6 +52,15 @@
</select>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="apiFormat" class="form-label">API格式</label>
<select class="form-select" id="apiFormat">
<option value="raw">原始API格式</option>
<option value="openai">OpenAI格式</option>
</select>
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="responseFormat" class="form-label">响应格式</label>

211
script.js
View File

@@ -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);